Error: '.$error);
-
- return;
- }
- } catch (\Throwable $e) {
- return handleError($e, $this);
- }
- }
-
- public function mount()
- {
- $this->parameters = get_route_parameters();
- }
-}
diff --git a/app/Livewire/Settings/Index.php b/app/Livewire/Settings/Index.php
index 754f0929b..2991b8ae8 100644
--- a/app/Livewire/Settings/Index.php
+++ b/app/Livewire/Settings/Index.php
@@ -5,62 +5,95 @@ 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\Attributes\Locked;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class Index extends Component
{
public InstanceSettings $settings;
- public bool $do_not_track;
-
- public bool $is_auto_update_enabled;
-
- public bool $is_registration_enabled;
-
- public bool $is_dns_validation_enabled;
-
- public bool $is_api_enabled;
-
- public string $auto_update_frequency;
-
- public string $update_check_frequency;
-
- protected string $dynamic_config_path = '/data/coolify/proxy/dynamic';
-
protected Server $server;
- protected $rules = [
- 'settings.fqdn' => 'nullable',
- 'settings.resale_license' => 'nullable',
- 'settings.public_port_min' => 'required',
- 'settings.public_port_max' => 'required',
- 'settings.custom_dns_servers' => 'nullable',
- 'settings.instance_name' => 'nullable',
- 'settings.allowed_ips' => 'nullable',
- 'settings.is_auto_update_enabled' => 'boolean',
- 'auto_update_frequency' => 'string',
- 'update_check_frequency' => 'string',
- 'settings.instance_timezone' => 'required|string|timezone',
- ];
-
- protected $validationAttributes = [
- 'settings.fqdn' => 'FQDN',
- 'settings.resale_license' => 'Resale License',
- 'settings.public_port_min' => 'Public port min',
- 'settings.public_port_max' => 'Public port max',
- 'settings.custom_dns_servers' => 'Custom DNS servers',
- 'settings.allowed_ips' => 'Allowed IPs',
- 'settings.is_auto_update_enabled' => 'Auto Update Enabled',
- 'auto_update_frequency' => 'Auto Update Frequency',
- 'update_check_frequency' => 'Update Check Frequency',
- ];
-
+ #[Locked]
public $timezones;
+ #[Validate('boolean')]
+ public bool $is_auto_update_enabled;
+
+ #[Validate('nullable|string|max:255')]
+ public ?string $fqdn = null;
+
+ #[Validate('nullable|string|max:255')]
+ public ?string $resale_license = null;
+
+ #[Validate('required|integer|min:1025|max:65535')]
+ public int $public_port_min;
+
+ #[Validate('required|integer|min:1025|max:65535')]
+ public int $public_port_max;
+
+ #[Validate('nullable|string')]
+ public ?string $custom_dns_servers = null;
+
+ #[Validate('nullable|string|max:255')]
+ public ?string $instance_name = null;
+
+ #[Validate('nullable|string')]
+ public ?string $allowed_ips = null;
+
+ #[Validate('nullable|string')]
+ public ?string $public_ipv4 = null;
+
+ #[Validate('nullable|string')]
+ public ?string $public_ipv6 = null;
+
+ #[Validate('string')]
+ public string $auto_update_frequency;
+
+ #[Validate('string')]
+ public string $update_check_frequency;
+
+ #[Validate('required|string|timezone')]
+ public string $instance_timezone;
+
+ #[Validate('boolean')]
+ public bool $do_not_track;
+
+ #[Validate('boolean')]
+ public bool $is_registration_enabled;
+
+ #[Validate('boolean')]
+ public bool $is_dns_validation_enabled;
+
+ #[Validate('boolean')]
+ public bool $is_api_enabled;
+
+ #[Validate('boolean')]
+ public bool $disable_two_step_confirmation;
+
+ public function render()
+ {
+ return view('livewire.settings.index');
+ }
+
public function mount()
{
- if (isInstanceAdmin()) {
+ if (! isInstanceAdmin()) {
+ return redirect()->route('dashboard');
+ } else {
$this->settings = instanceSettings();
+ $this->fqdn = $this->settings->fqdn;
+ $this->resale_license = $this->settings->resale_license;
+ $this->public_port_min = $this->settings->public_port_min;
+ $this->public_port_max = $this->settings->public_port_max;
+ $this->custom_dns_servers = $this->settings->custom_dns_servers;
+ $this->instance_name = $this->settings->instance_name;
+ $this->allowed_ips = $this->settings->allowed_ips;
+ $this->public_ipv4 = $this->settings->public_ipv4;
+ $this->public_ipv6 = $this->settings->public_ipv6;
$this->do_not_track = $this->settings->do_not_track;
$this->is_auto_update_enabled = $this->settings->is_auto_update_enabled;
$this->is_registration_enabled = $this->settings->is_registration_enabled;
@@ -69,13 +102,22 @@ class Index extends Component
$this->auto_update_frequency = $this->settings->auto_update_frequency;
$this->update_check_frequency = $this->settings->update_check_frequency;
$this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray();
- } else {
- return redirect()->route('dashboard');
+ $this->instance_timezone = $this->settings->instance_timezone;
+ $this->disable_two_step_confirmation = $this->settings->disable_two_step_confirmation;
}
}
- public function instantSave()
+ public function instantSave($isSave = true)
{
+ $this->settings->fqdn = $this->fqdn;
+ $this->settings->resale_license = $this->resale_license;
+ $this->settings->public_port_min = $this->public_port_min;
+ $this->settings->public_port_max = $this->public_port_max;
+ $this->settings->custom_dns_servers = $this->custom_dns_servers;
+ $this->settings->instance_name = $this->instance_name;
+ $this->settings->allowed_ips = $this->allowed_ips;
+ $this->settings->public_ipv4 = $this->public_ipv4;
+ $this->settings->public_ipv6 = $this->public_ipv6;
$this->settings->do_not_track = $this->do_not_track;
$this->settings->is_auto_update_enabled = $this->is_auto_update_enabled;
$this->settings->is_registration_enabled = $this->is_registration_enabled;
@@ -83,8 +125,12 @@ class Index extends Component
$this->settings->is_api_enabled = $this->is_api_enabled;
$this->settings->auto_update_frequency = $this->auto_update_frequency;
$this->settings->update_check_frequency = $this->update_check_frequency;
- $this->settings->save();
- $this->dispatch('success', 'Settings updated!');
+ $this->settings->disable_two_step_confirmation = $this->disable_two_step_confirmation;
+ $this->settings->instance_timezone = $this->instance_timezone;
+ if ($isSave) {
+ $this->settings->save();
+ $this->dispatch('success', 'Settings updated!');
+ }
}
public function submit()
@@ -141,13 +187,8 @@ class Index extends Component
$this->settings->allowed_ips = $this->settings->allowed_ips->unique();
$this->settings->allowed_ips = $this->settings->allowed_ips->implode(',');
- $this->settings->do_not_track = $this->do_not_track;
- $this->settings->is_auto_update_enabled = $this->is_auto_update_enabled;
- $this->settings->is_registration_enabled = $this->is_registration_enabled;
- $this->settings->is_dns_validation_enabled = $this->is_dns_validation_enabled;
- $this->settings->is_api_enabled = $this->is_api_enabled;
- $this->settings->auto_update_frequency = $this->auto_update_frequency;
- $this->settings->update_check_frequency = $this->update_check_frequency;
+ $this->instantSave(isSave: false);
+
$this->settings->save();
$this->server->setupDynamicProxyConfiguration();
if (! $error_show) {
@@ -170,15 +211,16 @@ class Index extends Component
}
}
- public function updatedSettingsInstanceTimezone($value)
+ public function toggleTwoStepConfirmation($password)
{
- $this->settings->instance_timezone = $value;
- $this->settings->save();
- $this->dispatch('success', 'Instance timezone updated.');
- }
+ if (! Hash::check($password, Auth::user()->password)) {
+ $this->addError('password', 'The provided password is incorrect.');
- public function render()
- {
- return view('livewire.settings.index');
+ return;
+ }
+
+ $this->settings->disable_two_step_confirmation = $this->disable_two_step_confirmation = true;
+ $this->settings->save();
+ $this->dispatch('success', 'Two step confirmation has been disabled.');
}
}
diff --git a/app/Livewire/Settings/License.php b/app/Livewire/Settings/License.php
index ca0c9c1ae..79f8269f3 100644
--- a/app/Livewire/Settings/License.php
+++ b/app/Livewire/Settings/License.php
@@ -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();
}
@@ -47,7 +50,6 @@ class License extends Component
$this->dispatch('reloadWindow');
} catch (\Throwable $e) {
session()->flash('error', 'Something went wrong. Please contact support. Error: '.$e->getMessage());
- ray($e->getMessage());
return redirect()->route('settings.license');
}
diff --git a/app/Livewire/SettingsBackup.php b/app/Livewire/SettingsBackup.php
index 9240aa96d..6dc5d6ab3 100644
--- a/app/Livewire/SettingsBackup.php
+++ b/app/Livewire/SettingsBackup.php
@@ -2,50 +2,59 @@
namespace App\Livewire;
-use App\Jobs\DatabaseBackupJob;
use App\Models\InstanceSettings;
use App\Models\S3Storage;
use App\Models\ScheduledDatabaseBackup;
use App\Models\Server;
use App\Models\StandalonePostgresql;
+use Livewire\Attributes\Locked;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class SettingsBackup extends Component
{
public InstanceSettings $settings;
- public $s3s;
-
public ?StandalonePostgresql $database = null;
public ScheduledDatabaseBackup|null|array $backup = [];
+ #[Locked]
+ public $s3s;
+
+ #[Locked]
public $executions = [];
- protected $rules = [
- 'database.uuid' => 'required',
- 'database.name' => 'required',
- 'database.description' => 'nullable',
- 'database.postgres_user' => 'required',
- 'database.postgres_password' => 'required',
+ #[Validate(['required'])]
+ public string $uuid;
- ];
+ #[Validate(['required'])]
+ public string $name;
- protected $validationAttributes = [
- 'database.uuid' => 'uuid',
- 'database.name' => 'name',
- 'database.description' => 'description',
- 'database.postgres_user' => 'postgres user',
- 'database.postgres_password' => 'postgres password',
- ];
+ #[Validate(['nullable'])]
+ public ?string $description = null;
+
+ #[Validate(['required'])]
+ public string $postgres_user;
+
+ #[Validate(['required'])]
+ public string $postgres_password;
public function mount()
{
- if (isInstanceAdmin()) {
+ if (! isInstanceAdmin()) {
+ return redirect()->route('dashboard');
+ } else {
$settings = instanceSettings();
$this->database = StandalonePostgresql::whereName('coolify-db')->first();
$s3s = S3Storage::whereTeamId(0)->get() ?? [];
if ($this->database) {
+ $this->uuid = $this->database->uuid;
+ $this->name = $this->database->name;
+ $this->description = $this->database->description;
+ $this->postgres_user = $this->database->postgres_user;
+ $this->postgres_password = $this->database->postgres_password;
+
if ($this->database->status !== 'running') {
$this->database->status = 'running';
$this->database->save();
@@ -55,13 +64,10 @@ class SettingsBackup extends Component
}
$this->settings = $settings;
$this->s3s = $s3s;
-
- } else {
- return redirect()->route('dashboard');
}
}
- public function add_coolify_database()
+ public function addCoolifyDatabase()
{
try {
$server = Server::findOrFail(0);
@@ -78,7 +84,7 @@ class SettingsBackup extends Component
'postgres_password' => $postgres_password,
'postgres_db' => $postgres_db,
'status' => 'running',
- 'destination_type' => 'App\Models\StandaloneDocker',
+ 'destination_type' => \App\Models\StandaloneDocker::class,
'destination_id' => 0,
]);
$this->backup = ScheduledDatabaseBackup::create([
@@ -87,7 +93,7 @@ class SettingsBackup extends Component
'save_s3' => false,
'frequency' => '0 0 * * *',
'database_id' => $this->database->id,
- 'database_type' => 'App\Models\StandalonePostgresql',
+ 'database_type' => \App\Models\StandalonePostgresql::class,
'team_id' => currentTeam()->id,
]);
$this->database->refresh();
@@ -98,16 +104,14 @@ class SettingsBackup extends Component
}
}
- public function backup_now()
- {
- dispatch(new DatabaseBackupJob(
- backup: $this->backup
- ));
- $this->dispatch('success', 'Backup queued. It will be available in a few minutes.');
- }
-
public function submit()
{
+ $this->database->update([
+ 'name' => $this->name,
+ 'description' => $this->description,
+ 'postgres_user' => $this->postgres_user,
+ 'postgres_password' => $this->postgres_password,
+ ]);
$this->dispatch('success', 'Backup updated.');
}
}
diff --git a/app/Livewire/SettingsEmail.php b/app/Livewire/SettingsEmail.php
index 4515df9a7..0ab5754f2 100644
--- a/app/Livewire/SettingsEmail.php
+++ b/app/Livewire/SettingsEmail.php
@@ -3,104 +3,83 @@
namespace App\Livewire;
use App\Models\InstanceSettings;
-use App\Notifications\TransactionalEmails\Test;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class SettingsEmail extends Component
{
public InstanceSettings $settings;
- public string $emails;
+ #[Validate(['boolean'])]
+ public bool $smtpEnabled = false;
- protected $rules = [
- 'settings.smtp_enabled' => 'nullable|boolean',
- 'settings.smtp_host' => 'required',
- 'settings.smtp_port' => 'required|numeric',
- 'settings.smtp_encryption' => 'nullable',
- 'settings.smtp_username' => 'nullable',
- 'settings.smtp_password' => 'nullable',
- 'settings.smtp_timeout' => 'nullable',
- 'settings.smtp_from_address' => 'required|email',
- 'settings.smtp_from_name' => 'required',
- 'settings.resend_enabled' => 'nullable|boolean',
- 'settings.resend_api_key' => 'nullable',
+ #[Validate(['nullable', 'string'])]
+ public ?string $smtpHost = null;
- ];
+ #[Validate(['nullable', 'numeric', 'min:1', 'max:65535'])]
+ public ?int $smtpPort = null;
- protected $validationAttributes = [
- 'settings.smtp_from_address' => 'From Address',
- 'settings.smtp_from_name' => 'From Name',
- 'settings.smtp_recipients' => 'Recipients',
- 'settings.smtp_host' => 'Host',
- 'settings.smtp_port' => 'Port',
- 'settings.smtp_encryption' => 'Encryption',
- 'settings.smtp_username' => 'Username',
- 'settings.smtp_password' => 'Password',
- 'settings.smtp_timeout' => 'Timeout',
- 'settings.resend_api_key' => 'Resend API Key',
- ];
+ #[Validate(['nullable', 'string'])]
+ public ?string $smtpEncryption = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $smtpUsername = null;
+
+ #[Validate(['nullable'])]
+ public ?string $smtpPassword = null;
+
+ #[Validate(['nullable', 'numeric'])]
+ public ?int $smtpTimeout = null;
+
+ #[Validate(['nullable', 'email'])]
+ public ?string $smtpFromAddress = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $smtpFromName = null;
+
+ #[Validate(['boolean'])]
+ public bool $resendEnabled = false;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $resendApiKey = null;
public function mount()
{
- if (isInstanceAdmin()) {
- $this->settings = instanceSettings();
- $this->emails = auth()->user()->email;
- } else {
+ if (isInstanceAdmin() === false) {
return redirect()->route('dashboard');
}
-
+ $this->settings = instanceSettings();
+ $this->syncData();
}
- public function submitFromFields()
+ public function syncData(bool $toModel = false)
{
- try {
- $this->resetErrorBag();
- $this->validate([
- 'settings.smtp_from_address' => 'required|email',
- 'settings.smtp_from_name' => 'required',
- ]);
+ if ($toModel) {
+ $this->validate();
+ $this->settings->smtp_enabled = $this->smtpEnabled;
+ $this->settings->smtp_host = $this->smtpHost;
+ $this->settings->smtp_port = $this->smtpPort;
+ $this->settings->smtp_encryption = $this->smtpEncryption;
+ $this->settings->smtp_username = $this->smtpUsername;
+ $this->settings->smtp_password = $this->smtpPassword;
+ $this->settings->smtp_timeout = $this->smtpTimeout;
+
+ $this->settings->resend_enabled = $this->resendEnabled;
+ $this->settings->resend_api_key = $this->resendApiKey;
$this->settings->save();
- $this->dispatch('success', 'Settings saved.');
- } catch (\Throwable $e) {
- return handleError($e, $this);
- }
- }
+ } else {
+ $this->smtpEnabled = $this->settings->smtp_enabled;
+ $this->smtpHost = $this->settings->smtp_host;
+ $this->smtpPort = $this->settings->smtp_port;
+ $this->smtpEncryption = $this->settings->smtp_encryption;
+ $this->smtpUsername = $this->settings->smtp_username;
+ $this->smtpPassword = $this->settings->smtp_password;
+ $this->smtpTimeout = $this->settings->smtp_timeout;
+ $this->smtpFromAddress = $this->settings->smtp_from_address;
+ $this->smtpFromName = $this->settings->smtp_from_name;
- public function submitResend()
- {
- try {
- $this->resetErrorBag();
- $this->validate([
- 'settings.smtp_from_address' => 'required|email',
- 'settings.smtp_from_name' => 'required',
- 'settings.resend_api_key' => 'required',
- ]);
- $this->settings->save();
- $this->dispatch('success', 'Settings saved.');
- } catch (\Throwable $e) {
- $this->settings->resend_enabled = false;
-
- return handleError($e, $this);
- }
- }
-
- public function instantSaveResend()
- {
- try {
- $this->settings->smtp_enabled = false;
- $this->submitResend();
- } catch (\Throwable $e) {
- return handleError($e, $this);
- }
- }
-
- public function instantSave()
- {
- try {
- $this->settings->resend_enabled = false;
- $this->submit();
- } catch (\Throwable $e) {
- return handleError($e, $this);
+ $this->resendEnabled = $this->settings->resend_enabled;
+ $this->resendApiKey = $this->settings->resend_api_key;
}
}
@@ -108,26 +87,29 @@ class SettingsEmail extends Component
{
try {
$this->resetErrorBag();
- $this->validate([
- 'settings.smtp_from_address' => 'required|email',
- 'settings.smtp_from_name' => 'required',
- 'settings.smtp_host' => 'required',
- 'settings.smtp_port' => 'required|numeric',
- 'settings.smtp_encryption' => 'nullable',
- 'settings.smtp_username' => 'nullable',
- 'settings.smtp_password' => 'nullable',
- 'settings.smtp_timeout' => 'nullable',
- ]);
- $this->settings->save();
+ $this->syncData(true);
$this->dispatch('success', 'Settings saved.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
- public function sendTestNotification()
+ public function instantSave(string $type)
{
- $this->settings?->notify(new Test($this->emails));
- $this->dispatch('success', 'Test email sent.');
+ try {
+ if ($type === 'SMTP') {
+ $this->resendEnabled = false;
+ } else {
+ $this->smtpEnabled = false;
+ }
+ $this->syncData(true);
+ if ($this->smtpEnabled || $this->resendEnabled) {
+ $this->dispatch('success', "{$type} enabled.");
+ } else {
+ $this->dispatch('success', "{$type} disabled.");
+ }
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
}
diff --git a/app/Livewire/SettingsOauth.php b/app/Livewire/SettingsOauth.php
index c3884589f..17b3b89a3 100644
--- a/app/Livewire/SettingsOauth.php
+++ b/app/Livewire/SettingsOauth.php
@@ -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;
diff --git a/app/Livewire/Source/Github/Change.php b/app/Livewire/Source/Github/Change.php
index 193b650ff..07cef54f9 100644
--- a/app/Livewire/Source/Github/Change.php
+++ b/app/Livewire/Source/Github/Change.php
@@ -4,7 +4,6 @@ namespace App\Livewire\Source\Github;
use App\Jobs\GithubAppPermissionJob;
use App\Models\GithubApp;
-use Illuminate\Support\Facades\Http;
use Livewire\Component;
class Change extends Component
@@ -93,51 +92,53 @@ class Change extends Component
// }
public function mount()
{
- $github_app_uuid = request()->github_app_uuid;
- $this->github_app = GithubApp::where('uuid', $github_app_uuid)->first();
- if (! $this->github_app) {
- return redirect()->route('source.all');
- }
- $this->applications = $this->github_app->applications;
- $settings = instanceSettings();
- $this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
+ try {
+ $github_app_uuid = request()->github_app_uuid;
+ $this->github_app = GithubApp::ownedByCurrentTeam()->whereUuid($github_app_uuid)->firstOrFail();
- $this->name = str($this->github_app->name)->kebab();
- $this->fqdn = $settings->fqdn;
+ $this->applications = $this->github_app->applications;
+ $settings = instanceSettings();
+ $this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
- if ($settings->public_ipv4) {
- $this->ipv4 = 'http://'.$settings->public_ipv4.':'.config('app.port');
- }
- if ($settings->public_ipv6) {
- $this->ipv6 = 'http://'.$settings->public_ipv6.':'.config('app.port');
- }
- if ($this->github_app->installation_id && session('from')) {
- $source_id = data_get(session('from'), 'source_id');
- if (! $source_id || $this->github_app->id !== $source_id) {
- session()->forget('from');
- } else {
- $parameters = data_get(session('from'), 'parameters');
- $back = data_get(session('from'), 'back');
- $environment_name = data_get($parameters, 'environment_name');
- $project_uuid = data_get($parameters, 'project_uuid');
- $type = data_get($parameters, 'type');
- $destination = data_get($parameters, 'destination');
- session()->forget('from');
+ $this->name = str($this->github_app->name)->kebab();
+ $this->fqdn = $settings->fqdn;
- return redirect()->route($back, [
- 'environment_name' => $environment_name,
- 'project_uuid' => $project_uuid,
- 'type' => $type,
- 'destination' => $destination,
- ]);
+ if ($settings->public_ipv4) {
+ $this->ipv4 = 'http://'.$settings->public_ipv4.':'.config('app.port');
}
- }
- $this->parameters = get_route_parameters();
- if (isCloud() && ! isDev()) {
- $this->webhook_endpoint = config('app.url');
- } else {
- $this->webhook_endpoint = $this->ipv4;
- $this->is_system_wide = $this->github_app->is_system_wide;
+ if ($settings->public_ipv6) {
+ $this->ipv6 = 'http://'.$settings->public_ipv6.':'.config('app.port');
+ }
+ if ($this->github_app->installation_id && session('from')) {
+ $source_id = data_get(session('from'), 'source_id');
+ if (! $source_id || $this->github_app->id !== $source_id) {
+ session()->forget('from');
+ } else {
+ $parameters = data_get(session('from'), 'parameters');
+ $back = data_get(session('from'), 'back');
+ $environment_name = data_get($parameters, 'environment_name');
+ $project_uuid = data_get($parameters, 'project_uuid');
+ $type = data_get($parameters, 'type');
+ $destination = data_get($parameters, 'destination');
+ session()->forget('from');
+
+ return redirect()->route($back, [
+ 'environment_name' => $environment_name,
+ 'project_uuid' => $project_uuid,
+ 'type' => $type,
+ 'destination' => $destination,
+ ]);
+ }
+ }
+ $this->parameters = get_route_parameters();
+ if (isCloud() && ! isDev()) {
+ $this->webhook_endpoint = config('app.url');
+ } else {
+ $this->webhook_endpoint = $this->ipv4;
+ $this->is_system_wide = $this->github_app->is_system_wide;
+ }
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
}
diff --git a/app/Livewire/Source/Github/Create.php b/app/Livewire/Source/Github/Create.php
index f85e8646e..103c5c9fb 100644
--- a/app/Livewire/Source/Github/Create.php
+++ b/app/Livewire/Source/Github/Create.php
@@ -23,7 +23,7 @@ class Create extends Component
public function mount()
{
- $this->name = generate_random_name();
+ $this->name = substr(generate_random_name(), 0, 34); // GitHub Apps names can only be 34 characters long
}
public function createGitHubApp()
diff --git a/app/Livewire/Subscription/PricingPlans.php b/app/Livewire/Subscription/PricingPlans.php
index 9bc11d862..6b2d3fb36 100644
--- a/app/Livewire/Subscription/PricingPlans.php
+++ b/app/Livewire/Subscription/PricingPlans.php
@@ -2,55 +2,23 @@
namespace App\Livewire\Subscription;
+use Illuminate\Support\Facades\Auth;
use Livewire\Component;
use Stripe\Checkout\Session;
use Stripe\Stripe;
class PricingPlans extends Component
{
- public bool $isTrial = false;
-
- public function mount()
- {
- $this->isTrial = ! data_get(currentTeam(), 'subscription.stripe_trial_already_ended');
- if (config('constants.limits.trial_period') == 0) {
- $this->isTrial = false;
- }
- }
-
public function subscribeStripe($type)
{
- $team = currentTeam();
Stripe::setApiKey(config('subscription.stripe_api_key'));
- switch ($type) {
- case 'basic-monthly':
- $priceId = config('subscription.stripe_price_id_basic_monthly');
- break;
- case 'basic-yearly':
- $priceId = config('subscription.stripe_price_id_basic_yearly');
- break;
- case 'pro-monthly':
- $priceId = config('subscription.stripe_price_id_pro_monthly');
- break;
- case 'pro-yearly':
- $priceId = config('subscription.stripe_price_id_pro_yearly');
- break;
- case 'ultimate-monthly':
- $priceId = config('subscription.stripe_price_id_ultimate_monthly');
- break;
- case 'ultimate-yearly':
- $priceId = config('subscription.stripe_price_id_ultimate_yearly');
- break;
- case 'dynamic-monthly':
- $priceId = config('subscription.stripe_price_id_dynamic_monthly');
- break;
- case 'dynamic-yearly':
- $priceId = config('subscription.stripe_price_id_dynamic_yearly');
- break;
- default:
- $priceId = config('subscription.stripe_price_id_basic_monthly');
- break;
- }
+
+ $priceId = match ($type) {
+ 'dynamic-monthly' => config('subscription.stripe_price_id_dynamic_monthly'),
+ 'dynamic-yearly' => config('subscription.stripe_price_id_dynamic_yearly'),
+ default => config('subscription.stripe_price_id_dynamic_monthly'),
+ };
+
if (! $priceId) {
$this->dispatch('error', 'Price ID not found! Please contact the administrator.');
@@ -59,10 +27,14 @@ class PricingPlans extends Component
$payload = [
'allow_promotion_codes' => true,
'billing_address_collection' => 'required',
- 'client_reference_id' => auth()->user()->id.':'.currentTeam()->id,
+ 'client_reference_id' => Auth::id().':'.currentTeam()->id,
'line_items' => [[
'price' => $priceId,
- 'quantity' => 1,
+ 'adjustable_quantity' => [
+ 'enabled' => true,
+ 'minimum' => 2,
+ ],
+ 'quantity' => 2,
]],
'tax_id_collection' => [
'enabled' => true,
@@ -70,39 +42,18 @@ class PricingPlans extends Component
'automatic_tax' => [
'enabled' => true,
],
-
+ 'subscription_data' => [
+ 'metadata' => [
+ 'user_id' => Auth::id(),
+ 'team_id' => currentTeam()->id,
+ ],
+ ],
+ 'payment_method_collection' => 'if_required',
'mode' => 'subscription',
'success_url' => route('dashboard', ['success' => true]),
'cancel_url' => route('subscription.index', ['cancelled' => true]),
];
- if (str($type)->contains('ultimate')) {
- $payload['line_items'][0]['adjustable_quantity'] = [
- 'enabled' => true,
- 'minimum' => 10,
- ];
- $payload['line_items'][0]['quantity'] = 10;
- }
- if (str($type)->contains('dynamic')) {
- $payload['line_items'][0]['adjustable_quantity'] = [
- 'enabled' => true,
- 'minimum' => 2,
- ];
- $payload['line_items'][0]['quantity'] = 2;
- }
- if (! data_get($team, 'subscription.stripe_trial_already_ended')) {
- if (config('constants.limits.trial_period') > 0) {
- $payload['subscription_data'] = [
- 'trial_period_days' => config('constants.limits.trial_period'),
- 'trial_settings' => [
- 'end_behavior' => [
- 'missing_payment_method' => 'cancel',
- ],
- ],
- ];
- }
- $payload['payment_method_collection'] = 'if_required';
- }
$customer = currentTeam()->subscription?->stripe_customer_id ?? null;
if ($customer) {
$payload['customer'] = $customer;
@@ -110,7 +61,7 @@ class PricingPlans extends Component
'name' => 'auto',
];
} else {
- $payload['customer_email'] = auth()->user()->email;
+ $payload['customer_email'] = Auth::user()->email;
}
$session = Session::create($payload);
diff --git a/app/Livewire/Tags/Deployments.php b/app/Livewire/Tags/Deployments.php
index 270aa176a..e4afa5b60 100644
--- a/app/Livewire/Tags/Deployments.php
+++ b/app/Livewire/Tags/Deployments.php
@@ -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);
}
diff --git a/app/Livewire/Tags/Index.php b/app/Livewire/Tags/Index.php
deleted file mode 100644
index a01d00a70..000000000
--- a/app/Livewire/Tags/Index.php
+++ /dev/null
@@ -1,79 +0,0 @@
- 'update_deployments'];
-
- public function update_deployments($deployments)
- {
- $this->deployments_per_tag_per_server = $deployments;
- }
-
- public function tag_updated()
- {
- if ($this->tag == '') {
- return;
- }
- $tag = $this->tags->where('name', $this->tag)->first();
- if (! $tag) {
- $this->dispatch('error', "Tag ({$this->tag}) not found.");
- $this->tag = '';
-
- return;
- }
- $this->webhook = generatTagDeployWebhook($tag->name);
- $this->applications = $tag->applications()->get();
- $this->services = $tag->services()->get();
- }
-
- public function redeploy_all()
- {
- try {
- $this->applications->each(function ($resource) {
- $deploy = new DeployController;
- $deploy->deploy_resource($resource);
- });
- $this->services->each(function ($resource) {
- $deploy = new DeployController;
- $deploy->deploy_resource($resource);
- });
- $this->dispatch('success', 'Mass deployment started.');
- } catch (\Exception $e) {
- 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');
- }
-}
diff --git a/app/Livewire/Tags/Show.php b/app/Livewire/Tags/Show.php
index 668101edb..fc5b13374 100644
--- a/app/Livewire/Tags/Show.php
+++ b/app/Livewire/Tags/Show.php
@@ -5,41 +5,57 @@ namespace App\Livewire\Tags;
use App\Http\Controllers\Api\DeployController;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Tag;
+use Illuminate\Support\Collection;
+use Livewire\Attributes\Locked;
+use Livewire\Attributes\Title;
use Livewire\Component;
+#[Title('Tags | Coolify')]
class Show extends Component
{
- public $tags;
+ #[Locked]
+ public ?string $tagName = null;
- public Tag $tag;
+ #[Locked]
+ public ?Collection $tags = null;
- public $applications;
+ #[Locked]
+ public ?Tag $tag = null;
- public $services;
+ #[Locked]
+ public ?Collection $applications = null;
- public $webhook = null;
+ #[Locked]
+ public ?Collection $services = null;
- public $deployments_per_tag_per_server = [];
+ #[Locked]
+ public ?string $webhook = null;
+
+ #[Locked]
+ public ?array $deploymentsPerTagPerServer = null;
public function mount()
{
- $this->tags = Tag::ownedByCurrentTeam()->get()->unique('name')->sortBy('name');
- $tag = $this->tags->where('name', request()->tag_name)->first();
- if (! $tag) {
- return redirect()->route('tags.index');
+ try {
+ $this->tags = Tag::ownedByCurrentTeam()->get()->unique('name')->sortBy('name');
+ if (str($this->tagName)->isNotEmpty()) {
+ $tag = $this->tags->where('name', $this->tagName)->first();
+ $this->webhook = generateTagDeployWebhook($tag->name);
+ $this->applications = $tag->applications()->get();
+ $this->services = $tag->services()->get();
+ $this->tag = $tag;
+ $this->getDeployments();
+ }
+ } catch (\Exception $e) {
+ return handleError($e, $this);
}
- $this->webhook = generatTagDeployWebhook($tag->name);
- $this->applications = $tag->applications()->get();
- $this->services = $tag->services()->get();
- $this->tag = $tag;
- $this->get_deployments();
}
- public function get_deployments()
+ public function getDeployments()
{
try {
$resource_ids = $this->applications->pluck('id');
- $this->deployments_per_tag_per_server = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('application_id', $resource_ids)->get([
+ $this->deploymentsPerTagPerServer = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('application_id', $resource_ids)->get([
'id',
'application_id',
'application_name',
@@ -54,7 +70,7 @@ class Show extends Component
}
}
- public function redeploy_all()
+ public function redeployAll()
{
try {
$message = collect([]);
diff --git a/app/Livewire/Team/AdminView.php b/app/Livewire/Team/AdminView.php
index 3026cb297..cfb47d9d8 100644
--- a/app/Livewire/Team/AdminView.php
+++ b/app/Livewire/Team/AdminView.php
@@ -2,6 +2,7 @@
namespace App\Livewire\Team;
+use App\Models\InstanceSettings;
use App\Models\Team;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
@@ -58,29 +59,30 @@ class AdminView extends Component
foreach ($servers as $server) {
$resources = $server->definedResources();
foreach ($resources as $resource) {
- ray('Deleting resource: '.$resource->name);
$resource->forceDelete();
}
- ray('Deleting server: '.$server->name);
$server->forceDelete();
}
$projects = $team->projects;
foreach ($projects as $project) {
- ray('Deleting project: '.$project->name);
$project->forceDelete();
}
$team->members()->detach($user->id);
- ray('Deleting team: '.$team->name);
$team->delete();
}
public function delete($id, $password)
{
- if (! Hash::check($password, Auth::user()->password)) {
- $this->addError('password', 'The provided password is incorrect.');
+ if (! isInstanceAdmin()) {
+ return redirect()->route('dashboard');
+ }
+ if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
+ if (! Hash::check($password, Auth::user()->password)) {
+ $this->addError('password', 'The provided password is incorrect.');
- return;
+ return;
+ }
}
if (! auth()->user()->isInstanceAdmin()) {
return $this->dispatch('error', 'You are not authorized to delete users');
@@ -88,29 +90,23 @@ class AdminView extends Component
$user = User::find($id);
$teams = $user->teams;
foreach ($teams as $team) {
- ray($team->name);
$user_alone_in_team = $team->members->count() === 1;
if ($team->id === 0) {
if ($user_alone_in_team) {
- ray('user is alone in the root team, do nothing');
-
return $this->dispatch('error', 'User is alone in the root team, cannot delete');
}
}
if ($user_alone_in_team) {
- ray('user is alone in the team');
$this->finalizeDeletion($user, $team);
continue;
}
- ray('user is not alone in the team');
if ($user->isOwner()) {
$found_other_owner_or_admin = $team->members->filter(function ($member) {
return $member->pivot->role === 'owner' || $member->pivot->role === 'admin';
})->where('id', '!=', $user->id)->first();
if ($found_other_owner_or_admin) {
- ray('found other owner or admin');
$team->members()->detach($user->id);
continue;
@@ -119,24 +115,19 @@ class AdminView extends Component
return $member->pivot->role === 'member';
})->first();
if ($found_other_member_who_is_not_owner) {
- ray('found other member who is not owner');
$found_other_member_who_is_not_owner->pivot->role = 'owner';
$found_other_member_who_is_not_owner->pivot->save();
$team->members()->detach($user->id);
} else {
- // This should never happen as if the user is the only member in the team, the team should be deleted already.
- ray('found no other member who is not owner');
$this->finalizeDeletion($user, $team);
}
continue;
}
} else {
- ray('user is not owner');
$team->members()->detach($user->id);
}
}
- ray('Deleting user: '.$user->name);
$user->delete();
$this->getUsers();
}
diff --git a/app/Livewire/Team/Create.php b/app/Livewire/Team/Create.php
index 992833da5..f805d6122 100644
--- a/app/Livewire/Team/Create.php
+++ b/app/Livewire/Team/Create.php
@@ -3,28 +3,21 @@
namespace App\Livewire\Team;
use App\Models\Team;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class Create extends Component
{
+ #[Validate(['required', 'min:3', 'max:255'])]
public string $name = '';
+ #[Validate(['nullable', 'min:3', 'max:255'])]
public ?string $description = null;
- protected $rules = [
- 'name' => 'required|min:3|max:255',
- 'description' => 'nullable|min:3|max:255',
- ];
-
- protected $validationAttributes = [
- 'name' => 'name',
- 'description' => 'description',
- ];
-
public function submit()
{
- $this->validate();
try {
+ $this->validate();
$team = Team::create([
'name' => $this->name,
'description' => $this->description,
diff --git a/app/Livewire/Team/Index.php b/app/Livewire/Team/Index.php
index 45600dbfe..0972e7364 100644
--- a/app/Livewire/Team/Index.php
+++ b/app/Livewire/Team/Index.php
@@ -4,6 +4,7 @@ namespace App\Livewire\Team;
use App\Models\Team;
use App\Models\TeamInvitation;
+use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Livewire\Component;
@@ -55,7 +56,7 @@ class Index extends Component
$currentTeam->delete();
$currentTeam->members->each(function ($user) use ($currentTeam) {
- if ($user->id === auth()->user()->id) {
+ if ($user->id === Auth::id()) {
return;
}
$user->teams()->detach($currentTeam);
diff --git a/app/Livewire/Team/Invitations.php b/app/Livewire/Team/Invitations.php
index 6a32a1d16..93432efc8 100644
--- a/app/Livewire/Team/Invitations.php
+++ b/app/Livewire/Team/Invitations.php
@@ -13,17 +13,18 @@ class Invitations extends Component
public function deleteInvitation(int $invitation_id)
{
- $initiation_found = TeamInvitation::find($invitation_id);
- if (! $initiation_found) {
+ try {
+ $initiation_found = TeamInvitation::ownedByCurrentTeam()->findOrFail($invitation_id);
+ $initiation_found->delete();
+ $this->refreshInvitations();
+ $this->dispatch('success', 'Invitation revoked.');
+ } catch (\Exception) {
return $this->dispatch('error', 'Invitation not found.');
}
- $initiation_found->delete();
- $this->refreshInvitations();
- $this->dispatch('success', 'Invitation revoked.');
}
public function refreshInvitations()
{
- $this->invitations = TeamInvitation::whereTeamId(currentTeam()->id)->get();
+ $this->invitations = TeamInvitation::ownedByCurrentTeam()->get();
}
}
diff --git a/app/Livewire/Team/InviteLink.php b/app/Livewire/Team/InviteLink.php
index 6c9e405fc..25f8a1ff5 100644
--- a/app/Livewire/Team/InviteLink.php
+++ b/app/Livewire/Team/InviteLink.php
@@ -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.'.');
diff --git a/app/Livewire/Team/Member.php b/app/Livewire/Team/Member.php
index 680cb901b..890d640a0 100644
--- a/app/Livewire/Team/Member.php
+++ b/app/Livewire/Team/Member.php
@@ -2,6 +2,7 @@
namespace App\Livewire\Team;
+use App\Enums\Role;
use App\Models\User;
use Illuminate\Support\Facades\Cache;
use Livewire\Component;
@@ -12,29 +13,66 @@ class Member extends Component
public function makeAdmin()
{
- $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'admin']);
- $this->dispatch('reloadWindow');
+ try {
+ if (Role::from(auth()->user()->role())->lt(Role::ADMIN)
+ || Role::from($this->getMemberRole())->gt(auth()->user()->role())) {
+ throw new \Exception('You are not authorized to perform this action.');
+ }
+ $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => Role::ADMIN->value]);
+ $this->dispatch('reloadWindow');
+ } catch (\Exception $e) {
+ $this->dispatch('error', $e->getMessage());
+ }
}
public function makeOwner()
{
- $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'owner']);
- $this->dispatch('reloadWindow');
+ try {
+ if (Role::from(auth()->user()->role())->lt(Role::OWNER)
+ || Role::from($this->getMemberRole())->gt(auth()->user()->role())) {
+ throw new \Exception('You are not authorized to perform this action.');
+ }
+ $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => Role::OWNER->value]);
+ $this->dispatch('reloadWindow');
+ } catch (\Exception $e) {
+ $this->dispatch('error', $e->getMessage());
+ }
}
public function makeReadonly()
{
- $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'member']);
- $this->dispatch('reloadWindow');
+ try {
+ if (Role::from(auth()->user()->role())->lt(Role::ADMIN)
+ || Role::from($this->getMemberRole())->gt(auth()->user()->role())) {
+ throw new \Exception('You are not authorized to perform this action.');
+ }
+ $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => Role::MEMBER->value]);
+ $this->dispatch('reloadWindow');
+ } catch (\Exception $e) {
+ $this->dispatch('error', $e->getMessage());
+ }
}
public function remove()
{
- $this->member->teams()->detach(currentTeam());
- Cache::forget("team:{$this->member->id}");
- Cache::remember('team:'.$this->member->id, 3600, function () {
- return $this->member->teams()->first();
- });
- $this->dispatch('reloadWindow');
+ try {
+ if (Role::from(auth()->user()->role())->lt(Role::ADMIN)
+ || Role::from($this->getMemberRole())->gt(auth()->user()->role())) {
+ throw new \Exception('You are not authorized to perform this action.');
+ }
+ $this->member->teams()->detach(currentTeam());
+ Cache::forget("team:{$this->member->id}");
+ Cache::remember('team:'.$this->member->id, 3600, function () {
+ return $this->member->teams()->first();
+ });
+ $this->dispatch('reloadWindow');
+ } catch (\Exception $e) {
+ $this->dispatch('error', $e->getMessage());
+ }
+ }
+
+ private function getMemberRole()
+ {
+ return $this->member->teams()->where('teams.id', currentTeam()->id)->first()?->pivot?->role;
}
}
diff --git a/app/Livewire/Upgrade.php b/app/Livewire/Upgrade.php
index dfbd945f5..88ed88cb7 100644
--- a/app/Livewire/Upgrade.php
+++ b/app/Livewire/Upgrade.php
@@ -23,11 +23,9 @@ class Upgrade extends Component
try {
$this->latestVersion = get_latest_version_of_coolify();
$this->isUpgradeAvailable = data_get(InstanceSettings::get(), 'new_version_available', false);
-
} catch (\Throwable $e) {
return handleError($e, $this);
}
-
}
public function upgrade()
diff --git a/app/Livewire/VerifyEmail.php b/app/Livewire/VerifyEmail.php
index d1f79c835..fab3265b6 100644
--- a/app/Livewire/VerifyEmail.php
+++ b/app/Livewire/VerifyEmail.php
@@ -15,10 +15,7 @@ class VerifyEmail extends Component
$this->rateLimit(1, 300);
auth()->user()->sendVerificationEmail();
$this->dispatch('success', 'Email verification link sent!');
-
} catch (\Exception $e) {
- ray($e);
-
return handleError($e, $this);
}
}
diff --git a/app/Models/Application.php b/app/Models/Application.php
index 07aeb4c5b..0ef787b2e 100644
--- a/app/Models/Application.php
+++ b/app/Models/Application.php
@@ -114,17 +114,34 @@ class Application extends BaseModel
protected static function booted()
{
static::saving(function ($application) {
- if ($application->fqdn == '') {
+ if ($application->fqdn === '') {
$application->fqdn = null;
}
- $application->forceFill([
- 'fqdn' => $application->fqdn,
- 'install_command' => str($application->install_command)->trim(),
- 'build_command' => str($application->build_command)->trim(),
- 'start_command' => str($application->start_command)->trim(),
- 'base_directory' => str($application->base_directory)->trim(),
- 'publish_directory' => str($application->publish_directory)->trim(),
- ]);
+ $payload = [];
+ if ($application->isDirty('fqdn')) {
+ $payload['fqdn'] = $application->fqdn;
+ }
+ if ($application->isDirty('install_command')) {
+ $payload['install_command'] = str($application->install_command)->trim();
+ }
+ if ($application->isDirty('build_command')) {
+ $payload['build_command'] = str($application->build_command)->trim();
+ }
+ if ($application->isDirty('start_command')) {
+ $payload['start_command'] = str($application->start_command)->trim();
+ }
+ if ($application->isDirty('base_directory')) {
+ $payload['base_directory'] = str($application->base_directory)->trim();
+ }
+ if ($application->isDirty('publish_directory')) {
+ $payload['publish_directory'] = str($application->publish_directory)->trim();
+ }
+ if ($application->isDirty('status')) {
+ $payload['last_online_at'] = now();
+ }
+ if (count($payload) > 0) {
+ $application->forceFill($payload);
+ }
});
static::created(function ($application) {
ApplicationSetting::create([
@@ -155,6 +172,11 @@ class Application extends BaseModel
return Application::whereRelation('environment.project.team', 'id', $teamId)->orderBy('name');
}
+ public static function ownedByCurrentTeam()
+ {
+ return Application::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
+ }
+
public function getContainersToStop(bool $previewDeployments = false): array
{
$containers = $previewDeployments
@@ -221,7 +243,6 @@ class Application extends BaseModel
{
if ($this->build_pack === 'dockercompose') {
$server = data_get($this, 'destination.server');
- ray('Deleting volumes');
instant_remote_process(["cd {$this->dirOnServer()} && docker compose down -v"], $server, false);
} else {
if ($persistentStorages->count() === 0) {
@@ -937,7 +958,7 @@ class Application extends BaseModel
$source_html_url_host = $url['host'];
$source_html_url_scheme = $url['scheme'];
- if ($this->source->getMorphClass() == 'App\Models\GithubApp') {
+ if ($this->source->getMorphClass() === \App\Models\GithubApp::class) {
if ($this->source->is_public) {
$fullRepoUrl = "{$this->source->html_url}/{$customRepository}";
$git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$customRepository} {$baseDir}";
@@ -1246,13 +1267,11 @@ class Application extends BaseModel
return;
}
if (base64_encode(base64_decode($customLabels, true)) !== $customLabels) {
- ray('custom_labels is not base64 encoded');
$this->custom_labels = str($customLabels)->replace(',', "\n");
$this->custom_labels = base64_encode($customLabels);
}
$customLabels = base64_decode($this->custom_labels);
if (mb_detect_encoding($customLabels, 'ASCII', true) === false) {
- ray('custom_labels contains non-ascii characters');
$customLabels = str(implode('|coolify|', generateLabelsApplication($this, $preview)))->replace('|coolify|', "\n");
}
$this->custom_labels = base64_encode($customLabels);
@@ -1400,29 +1419,48 @@ class Application extends BaseModel
return [];
}
- public function getMetrics(int $mins = 5)
+ public function getCpuMetrics(int $mins = 5)
{
$server = $this->destination->server;
$container_name = $this->uuid;
if ($server->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString();
- $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
if (str($metrics)->contains('error')) {
$error = json_decode($metrics, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
- if ($error == 'Unauthorized') {
+ if ($error === 'Unauthorized') {
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
}
throw new \Exception($error);
}
- $metrics = str($metrics)->explode("\n")->skip(1)->all();
- $parsedCollection = collect($metrics)->flatMap(function ($item) {
- return collect(explode("\n", trim($item)))->map(function ($line) {
- [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line));
- $cpu_usage_percent = number_format($cpu_usage_percent, 2);
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['percent']];
+ });
- return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage];
- });
+ return $parsedCollection->toArray();
+ }
+ }
+
+ public function getMemoryMetrics(int $mins = 5)
+ {
+ $server = $this->destination->server;
+ $container_name = $this->uuid;
+ if ($server->isMetricsEnabled()) {
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
+ }
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['used']];
});
return $parsedCollection->toArray();
@@ -1459,9 +1497,9 @@ class Application extends BaseModel
return $config;
}
- public function setConfig($config) {
- $config = $config;
+ public function setConfig($config)
+ {
$validator = Validator::make(['config' => $config], [
'config' => 'required|json',
]);
diff --git a/app/Models/ApplicationPreview.php b/app/Models/ApplicationPreview.php
index 04a0ab27e..bf2bf05bf 100644
--- a/app/Models/ApplicationPreview.php
+++ b/app/Models/ApplicationPreview.php
@@ -28,6 +28,11 @@ class ApplicationPreview extends BaseModel
});
}
});
+ static::saving(function ($preview) {
+ if ($preview->isDirty('status')) {
+ $preview->forceFill(['last_online_at' => now()]);
+ }
+ });
}
public static function findPreviewByApplicationAndPullId(int $application_id, int $pull_request_id)
diff --git a/app/Models/Environment.php b/app/Models/Environment.php
index c892d7ba1..71e8bbd21 100644
--- a/app/Models/Environment.php
+++ b/app/Models/Environment.php
@@ -27,10 +27,8 @@ class Environment extends Model
static::deleting(function ($environment) {
$shared_variables = $environment->environment_variables();
foreach ($shared_variables as $shared_variable) {
- ray('Deleting environment shared variable: '.$shared_variable->name);
$shared_variable->delete();
}
-
});
}
diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php
index 9f8e4b342..08f23d7ab 100644
--- a/app/Models/EnvironmentVariable.php
+++ b/app/Models/EnvironmentVariable.php
@@ -44,7 +44,7 @@ class EnvironmentVariable extends Model
'version' => 'string',
];
- protected $appends = ['real_value', 'is_shared'];
+ protected $appends = ['real_value', 'is_shared', 'is_really_required'];
protected static function booted()
{
@@ -74,6 +74,9 @@ class EnvironmentVariable extends Model
'version' => config('version'),
]);
});
+ static::saving(function (EnvironmentVariable $environmentVariable) {
+ $environmentVariable->updateIsShared();
+ });
}
public function service()
@@ -130,6 +133,13 @@ class EnvironmentVariable extends Model
);
}
+ protected function isReallyRequired(): Attribute
+ {
+ return Attribute::make(
+ get: fn () => $this->is_required && str($this->real_value)->isEmpty(),
+ );
+ }
+
protected function isShared(): Attribute
{
return Attribute::make(
@@ -146,13 +156,12 @@ class EnvironmentVariable extends Model
private function get_real_environment_variables(?string $environment_variable = null, $resource = null)
{
- if ((is_null($environment_variable) && $environment_variable == '') || is_null($resource)) {
+ if ((is_null($environment_variable) && $environment_variable === '') || is_null($resource)) {
return null;
}
$environment_variable = trim($environment_variable);
$sharedEnvsFound = str($environment_variable)->matchAll('/{{(.*?)}}/');
if ($sharedEnvsFound->isEmpty()) {
-
return $environment_variable;
}
@@ -192,7 +201,7 @@ class EnvironmentVariable extends Model
private function set_environment_variables(?string $environment_variable = null): ?string
{
- if (is_null($environment_variable) && $environment_variable == '') {
+ if (is_null($environment_variable) && $environment_variable === '') {
return null;
}
$environment_variable = trim($environment_variable);
@@ -210,4 +219,11 @@ class EnvironmentVariable extends Model
set: fn (string $value) => str($value)->trim()->replace(' ', '_')->value,
);
}
+
+ protected function updateIsShared(): void
+ {
+ $type = str($this->value)->after('{{')->before('.')->value;
+ $isShared = str($this->value)->startsWith('{{'.$type) && str($this->value)->endsWith('}}');
+ $this->is_shared = $isShared;
+ }
}
diff --git a/app/Models/GithubApp.php b/app/Models/GithubApp.php
index 66ecdd967..0b0e93b12 100644
--- a/app/Models/GithubApp.php
+++ b/app/Models/GithubApp.php
@@ -31,6 +31,11 @@ class GithubApp extends BaseModel
});
}
+ public static function ownedByCurrentTeam()
+ {
+ return GithubApp::whereTeamId(currentTeam()->id);
+ }
+
public static function public()
{
return GithubApp::whereTeamId(currentTeam()->id)->whereisPublic(true)->whereNotNull('app_id')->get();
@@ -60,7 +65,7 @@ class GithubApp extends BaseModel
{
return Attribute::make(
get: function () {
- if ($this->getMorphClass() === 'App\Models\GithubApp') {
+ if ($this->getMorphClass() === \App\Models\GithubApp::class) {
return 'github';
}
},
diff --git a/app/Models/GitlabApp.php b/app/Models/GitlabApp.php
index a789a7e65..2112a4a66 100644
--- a/app/Models/GitlabApp.php
+++ b/app/Models/GitlabApp.php
@@ -9,6 +9,11 @@ class GitlabApp extends BaseModel
'app_secret',
];
+ public static function ownedByCurrentTeam()
+ {
+ return GitlabApp::whereTeamId(currentTeam()->id);
+ }
+
public function applications()
{
return $this->morphMany(Application::class, 'source');
diff --git a/app/Models/InstanceSettings.php b/app/Models/InstanceSettings.php
index bb3d1478b..eeb803925 100644
--- a/app/Models/InstanceSettings.php
+++ b/app/Models/InstanceSettings.php
@@ -2,6 +2,7 @@
namespace App\Models;
+use App\Jobs\PullHelperImageJob;
use App\Notifications\Channels\SendsEmail;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
@@ -21,8 +22,22 @@ class InstanceSettings extends Model implements SendsEmail
'is_auto_update_enabled' => 'boolean',
'auto_update_frequency' => 'string',
'update_check_frequency' => 'string',
+ 'sentinel_token' => 'encrypted',
];
+ protected static function booted(): void
+ {
+ static::updated(function ($settings) {
+ if ($settings->isDirty('helper_version')) {
+ Server::chunkById(100, function ($servers) {
+ foreach ($servers as $server) {
+ PullHelperImageJob::dispatch($server);
+ }
+ });
+ }
+ });
+ }
+
public function fqdn(): Attribute
{
return Attribute::make(
@@ -86,16 +101,16 @@ class InstanceSettings extends Model implements SendsEmail
return "[{$instanceName}]";
}
- public function helperVersion(): Attribute
- {
- return Attribute::make(
- get: function ($value) {
- if (isDev()) {
- return 'latest';
- }
+ // public function helperVersion(): Attribute
+ // {
+ // return Attribute::make(
+ // get: function ($value) {
+ // if (isDev()) {
+ // return 'latest';
+ // }
- return $value;
- }
- );
- }
+ // return $value;
+ // }
+ // );
+ // }
}
diff --git a/app/Models/LocalFileVolume.php b/app/Models/LocalFileVolume.php
index d528099ff..2c223be77 100644
--- a/app/Models/LocalFileVolume.php
+++ b/app/Models/LocalFileVolume.php
@@ -72,7 +72,6 @@ class LocalFileVolume extends BaseModel
if ($path && $path != '/' && $path != '.' && $path != '..') {
if ($isFile === 'OK') {
$commands->push("rm -rf $path > /dev/null 2>&1 || true");
-
} elseif ($isDir === 'OK') {
$commands->push("rm -rf $path > /dev/null 2>&1 || true");
$commands->push("rmdir $path > /dev/null 2>&1 || true");
@@ -113,15 +112,15 @@ class LocalFileVolume extends BaseModel
}
$isFile = instant_remote_process(["test -f $path && echo OK || echo NOK"], $server);
$isDir = instant_remote_process(["test -d $path && echo OK || echo NOK"], $server);
- if ($isFile == 'OK' && $this->is_directory) {
+ if ($isFile === 'OK' && $this->is_directory) {
$content = instant_remote_process(["cat $path"], $server, false);
$this->is_directory = false;
$this->content = $content;
$this->save();
FileStorageChanged::dispatch(data_get($server, 'team_id'));
throw new \Exception('The following file is a file on the server, but you are trying to mark it as a directory. Please delete the file on the server or mark it as directory.');
- } elseif ($isDir == 'OK' && ! $this->is_directory) {
- if ($path == '/' || $path == '.' || $path == '..' || $path == '' || str($path)->isEmpty() || is_null($path)) {
+ } elseif ($isDir === 'OK' && ! $this->is_directory) {
+ if ($path === '/' || $path === '.' || $path === '..' || $path === '' || str($path)->isEmpty() || is_null($path)) {
$this->is_directory = true;
$this->save();
throw new \Exception('The following file is a directory on the server, but you are trying to mark it as a file.
Please delete the directory on the server or mark it as directory.');
@@ -132,7 +131,7 @@ class LocalFileVolume extends BaseModel
], $server, false);
FileStorageChanged::dispatch(data_get($server, 'team_id'));
}
- if ($isDir == 'NOK' && ! $this->is_directory) {
+ if ($isDir === 'NOK' && ! $this->is_directory) {
$chmod = data_get($this, 'chmod');
$chown = data_get($this, 'chown');
if ($content) {
@@ -148,7 +147,7 @@ class LocalFileVolume extends BaseModel
if ($chmod) {
$commands->push("chmod $chmod $path");
}
- } elseif ($isDir == 'NOK' && $this->is_directory) {
+ } elseif ($isDir === 'NOK' && $this->is_directory) {
$commands->push("mkdir -p $path > /dev/null 2>&1 || true");
}
diff --git a/app/Models/Project.php b/app/Models/Project.php
index 5a9dd964a..f27e6c208 100644
--- a/app/Models/Project.php
+++ b/app/Models/Project.php
@@ -47,7 +47,6 @@ class Project extends BaseModel
$project->settings()->delete();
$shared_variables = $project->environment_variables();
foreach ($shared_variables as $shared_variable) {
- ray('Deleting project shared variable: '.$shared_variable->name);
$shared_variable->delete();
}
});
@@ -123,9 +122,18 @@ class Project extends BaseModel
return $this->hasManyThrough(StandaloneMariadb::class, Environment::class);
}
- public function resource_count()
+ public function isEmpty()
{
- return $this->applications()->count() + $this->postgresqls()->count() + $this->redis()->count() + $this->mongodbs()->count() + $this->mysqls()->count() + $this->mariadbs()->count() + $this->keydbs()->count() + $this->dragonflies()->count() + $this->clickhouses()->count() + $this->services()->count();
+ return $this->applications()->count() == 0 &&
+ $this->redis()->count() == 0 &&
+ $this->postgresqls()->count() == 0 &&
+ $this->mysqls()->count() == 0 &&
+ $this->keydbs()->count() == 0 &&
+ $this->dragonflies()->count() == 0 &&
+ $this->clickhouses()->count() == 0 &&
+ $this->mariadbs()->count() == 0 &&
+ $this->mongodbs()->count() == 0 &&
+ $this->services()->count() == 0;
}
public function databases()
diff --git a/app/Models/ScheduledDatabaseBackup.php b/app/Models/ScheduledDatabaseBackup.php
index 3921e32e4..473fc7b4b 100644
--- a/app/Models/ScheduledDatabaseBackup.php
+++ b/app/Models/ScheduledDatabaseBackup.php
@@ -51,7 +51,6 @@ class ScheduledDatabaseBackup extends BaseModel
}
}
-
return null;
}
}
diff --git a/app/Models/ScheduledTask.php b/app/Models/ScheduledTask.php
index 3cee5a875..264a04d1f 100644
--- a/app/Models/ScheduledTask.php
+++ b/app/Models/ScheduledTask.php
@@ -34,21 +34,15 @@ class ScheduledTask extends BaseModel
{
if ($this->application) {
if ($this->application->destination && $this->application->destination->server) {
- $server = $this->application->destination->server;
-
- return $server;
+ return $this->application->destination->server;
}
} elseif ($this->service) {
if ($this->service->destination && $this->service->destination->server) {
- $server = $this->service->destination->server;
-
- return $server;
+ return $this->service->destination->server;
}
} elseif ($this->database) {
if ($this->database->destination && $this->database->destination->server) {
- $server = $this->database->destination->server;
-
- return $server;
+ return $this->database->destination->server;
}
}
diff --git a/app/Models/Server.php b/app/Models/Server.php
index 0eca3c168..3076308ad 100644
--- a/app/Models/Server.php
+++ b/app/Models/Server.php
@@ -3,13 +3,17 @@
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;
+use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
-use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Stringable;
use OpenApi\Attributes as OA;
@@ -43,7 +47,7 @@ use Symfony\Component\Yaml\Yaml;
class Server extends BaseModel
{
- use SchemalessAttributesTrait;
+ use SchemalessAttributesTrait, SoftDeletes;
public static $batch_counter = 0;
@@ -59,6 +63,11 @@ class Server extends BaseModel
}
$server->forceFill($payload);
});
+ static::saved(function ($server) {
+ if ($server->privateKey->isDirty()) {
+ refresh_server_connection($server->privateKey);
+ }
+ });
static::created(function ($server) {
ServerSetting::create([
'server_id' => $server->id,
@@ -95,7 +104,8 @@ class Server extends BaseModel
}
}
});
- static::deleting(function ($server) {
+
+ static::forceDeleting(function ($server) {
$server->destinations()->each(function ($destination) {
$destination->delete();
});
@@ -103,12 +113,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 = [
@@ -127,6 +140,11 @@ class Server extends BaseModel
protected $guarded = [];
+ public function type()
+ {
+ return 'server';
+ }
+
public static function isReachable()
{
return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true);
@@ -401,7 +419,7 @@ respond 404
"echo '$base64' | base64 -d | tee $file > /dev/null",
], $this);
- if (config('app.env') == 'local') {
+ if (config('app.env') === 'local') {
// ray($yaml);
}
}
@@ -489,20 +507,6 @@ $schema://$host {
return Server::whereTeamId($teamId)->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_build_server', true);
}
- public function skipServer()
- {
- if ($this->ip === '1.2.3.4') {
- // ray('skipping 1.2.3.4');
- return true;
- }
- if ($this->settings->force_disabled === true) {
- // ray('force_disabled');
- return true;
- }
-
- return false;
- }
-
public function isForceDisabled()
{
return $this->settings->force_disabled;
@@ -510,24 +514,48 @@ $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());
}
+ public function sentinelHeartbeat(bool $isReset = false)
+ {
+ $this->sentinel_updated_at = $isReset ? now()->subMinutes(6000) : now();
+ $this->save();
+ }
+
+ /**
+ * Get the wait time for Sentinel to push before performing an SSH check.
+ *
+ * @return int The wait time in seconds.
+ */
+ public function waitBeforeDoingSshCheck(): int
+ {
+ $wait = $this->settings->sentinel_push_interval_seconds * 3;
+ if ($wait < 120) {
+ $wait = 120;
+ }
+
+ return $wait;
+ }
+
+ public function isSentinelLive()
+ {
+ return Carbon::parse($this->sentinel_updated_at)->isAfter(now()->subSeconds($this->waitBeforeDoingSshCheck()));
+ }
+
public function isSentinelEnabled()
{
- return $this->isMetricsEnabled() || $this->isServerApiEnabled();
+ return ($this->isMetricsEnabled() || $this->isServerApiEnabled()) && ! $this->isBuildServer();
}
public function isMetricsEnabled()
@@ -537,68 +565,32 @@ $schema://$host {
public function isServerApiEnabled()
{
- return $this->settings->is_server_api_enabled;
- }
-
- public function checkServerApi()
- {
- if ($this->isServerApiEnabled()) {
- $server_ip = $this->ip;
- if (isDev()) {
- if ($this->id === 0) {
- $server_ip = 'localhost';
- }
- }
- $command = "curl -s http://{$server_ip}:12172/api/health";
- $process = Process::timeout(5)->run($command);
- if ($process->failed()) {
- ray($process->exitCode(), $process->output(), $process->errorOutput());
- throw new \Exception("Server API is not reachable on http://{$server_ip}:12172");
- }
-
- }
+ 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)
{
if ($this->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString();
- $cpu = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->metrics_token}\" http://localhost:8888/api/cpu/history?from=$from'"], $this, false);
+ $cpu = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://localhost:8888/api/cpu/history?from=$from'"], $this, false);
if (str($cpu)->contains('error')) {
$error = json_decode($cpu, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
- if ($error == 'Unauthorized') {
+ if ($error === 'Unauthorized') {
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
}
throw new \Exception($error);
}
- $cpu = str($cpu)->explode("\n")->skip(1)->all();
- $parsedCollection = collect($cpu)->flatMap(function ($item) {
- return collect(explode("\n", trim($item)))->map(function ($line) {
- [$time, $cpu_usage_percent] = explode(',', trim($line));
- $cpu_usage_percent = number_format($cpu_usage_percent, 0);
+ $cpu = json_decode($cpu, true);
- return [(int) $time, (float) $cpu_usage_percent];
- });
+ return collect($cpu)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['percent']];
});
-
- return $parsedCollection->toArray();
}
}
@@ -606,98 +598,28 @@ $schema://$host {
{
if ($this->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString();
- $memory = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->metrics_token}\" http://localhost:8888/api/memory/history?from=$from'"], $this, false);
+ $memory = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://localhost:8888/api/memory/history?from=$from'"], $this, false);
if (str($memory)->contains('error')) {
$error = json_decode($memory, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
- if ($error == 'Unauthorized') {
+ if ($error === 'Unauthorized') {
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
}
throw new \Exception($error);
}
- $memory = str($memory)->explode("\n")->skip(1)->all();
- $parsedCollection = collect($memory)->flatMap(function ($item) {
- return collect(explode("\n", trim($item)))->map(function ($line) {
- [$time, $used, $free, $usedPercent] = explode(',', trim($line));
- $usedPercent = number_format($usedPercent, 0);
-
- return [(int) $time, (float) $usedPercent];
- });
+ $memory = json_decode($memory, true);
+ $parsedCollection = collect($memory)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['usedPercent']];
});
return $parsedCollection->toArray();
}
}
- 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 /| tail -1 | awk '{ print $5}' | sed 's/%//g'"], $this, false);
+ return instant_remote_process(['df / --output=pcent | tr -cd 0-9'], $this, false);
+ // return instant_remote_process(["df /| tail -1 | awk '{ print $5}' | sed 's/%//g'"], $this, false);
}
public function definedResources()
@@ -755,7 +677,7 @@ $schema://$host {
}
}
} else {
- $containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this, false);
+ $containers = instant_remote_process(["docker container inspect $(docker container ls -aq) --format '{{json .}}'"], $this, false);
$containers = format_docker_command_output_to_json($containers);
$containerReplicates = collect([]);
}
@@ -899,9 +821,7 @@ $schema://$host {
{
return Attribute::make(
get: function ($value) {
- $sanitizedValue = preg_replace('/[^A-Za-z0-9\-_]/', '', $value);
-
- return $sanitizedValue;
+ return preg_replace('/[^A-Za-z0-9\-_]/', '', $value);
}
);
}
@@ -945,8 +865,6 @@ $schema://$host {
$standalone_docker = $this->hasMany(StandaloneDocker::class)->get();
$swarm_docker = $this->hasMany(SwarmDocker::class)->get();
- // $additional_dockers = $this->belongsToMany(StandaloneDocker::class, 'additional_destinations')->withPivot('server_id')->get();
- // return $standalone_docker->concat($swarm_docker)->concat($additional_dockers);
return $standalone_docker->concat($swarm_docker);
}
@@ -977,18 +895,31 @@ $schema://$host {
public function isProxyShouldRun()
{
- if ($this->proxyType() === ProxyTypes::NONE->value || $this->settings->is_build_server) {
+ // TODO: Do we need "|| $this->proxy->force_stop" here?
+ if ($this->proxyType() === ProxyTypes::NONE->value || $this->isBuildServer()) {
return false;
}
return true;
}
+ public function skipServer()
+ {
+ if ($this->ip === '1.2.3.4') {
+ return true;
+ }
+ if ($this->settings->force_disabled === true) {
+ return true;
+ }
+
+ return false;
+ }
+
public function isFunctional()
{
- $isFunctional = $this->settings->is_reachable && $this->settings->is_usable && ! $this->settings->force_disabled;
+ $isFunctional = $this->settings->is_reachable && $this->settings->is_usable && $this->settings->force_disabled === false && $this->ip !== '1.2.3.4';
- if (! $isFunctional) {
+ if ($isFunctional === false) {
Storage::disk('ssh-mux')->delete($this->muxFilename());
}
@@ -1041,39 +972,110 @@ $schema://$host {
return data_get($this, 'settings.is_swarm_worker');
}
- public function validateConnection($isManualCheck = true)
+ public function serverStatus(): bool
+ {
+ if ($this->status() === false) {
+ return false;
+ }
+ if ($this->isFunctional() === false) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public function status(): bool
+ {
+ ['uptime' => $uptime] = $this->validateConnection(false);
+ if ($uptime === false) {
+ foreach ($this->applications() as $application) {
+ $application->status = 'exited';
+ $application->save();
+ }
+ foreach ($this->databases() as $database) {
+ $database->status = 'exited';
+ $database->save();
+ }
+ foreach ($this->services() as $service) {
+ $apps = $service->applications()->get();
+ $dbs = $service->databases()->get();
+ foreach ($apps as $app) {
+ $app->status = 'exited';
+ $app->save();
+ }
+ foreach ($dbs as $db) {
+ $db->status = 'exited';
+ $db->save();
+ }
+ }
+
+ return false;
+ }
+
+ return true;
+ }
+
+ public function isReachableChanged()
+ {
+ $this->refresh();
+ $unreachableNotificationSent = (bool) $this->unreachable_notification_sent;
+ $isReachable = (bool) $this->settings->is_reachable;
+ // 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(bool $isManualCheck = true, bool $justCheckingNewKey = false)
{
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 ($justCheckingNewKey) {
+ return ['uptime' => false, 'error' => 'This key is not valid for this server.'];
+ }
+ if ($this->settings->is_reachable === true) {
+ $this->settings->is_reachable = false;
+ $this->settings->save();
+ }
return ['uptime' => false, 'error' => $e->getMessage()];
}
@@ -1081,9 +1083,7 @@ $schema://$host {
public function installDocker()
{
- $activity = InstallDocker::run($this);
-
- return $activity;
+ return InstallDocker::run($this);
}
public function validateDockerEngine($throwError = false)
@@ -1228,4 +1228,27 @@ $schema://$host {
{
return str($this->ip)->contains(':');
}
+
+ public function restartSentinel(bool $async = true)
+ {
+ try {
+ if ($async) {
+ StartSentinel::dispatch($this, true);
+ } else {
+ StartSentinel::run($this, true);
+ }
+ } catch (\Throwable $e) {
+ return handleError($e);
+ }
+ }
+
+ public function url()
+ {
+ return base_url().'/server/'.$this->uuid;
+ }
+
+ public function restartContainer(string $containerName)
+ {
+ return instant_remote_process(['docker restart '.$containerName], $this, false);
+ }
}
diff --git a/app/Models/ServerSetting.php b/app/Models/ServerSetting.php
index c44a393b4..fc2c5a0f4 100644
--- a/app/Models/ServerSetting.php
+++ b/app/Models/ServerSetting.php
@@ -4,6 +4,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
+use Illuminate\Support\Facades\Log;
use OpenApi\Attributes as OA;
#[OA\Schema(
@@ -24,7 +25,7 @@ use OpenApi\Attributes as OA;
'is_logdrain_newrelic_enabled' => ['type' => 'boolean'],
'is_metrics_enabled' => ['type' => 'boolean'],
'is_reachable' => ['type' => 'boolean'],
- 'is_server_api_enabled' => ['type' => 'boolean'],
+ 'is_sentinel_enabled' => ['type' => 'boolean'],
'is_swarm_manager' => ['type' => 'boolean'],
'is_swarm_worker' => ['type' => 'boolean'],
'is_usable' => ['type' => 'boolean'],
@@ -35,9 +36,9 @@ use OpenApi\Attributes as OA;
'logdrain_highlight_project_id' => ['type' => 'string'],
'logdrain_newrelic_base_uri' => ['type' => 'string'],
'logdrain_newrelic_license_key' => ['type' => 'string'],
- 'metrics_history_days' => ['type' => 'integer'],
- 'metrics_refresh_rate_seconds' => ['type' => 'integer'],
- 'metrics_token' => ['type' => 'string'],
+ 'sentinel_metrics_history_days' => ['type' => 'integer'],
+ 'sentinel_metrics_refresh_rate_seconds' => ['type' => 'integer'],
+ 'sentinel_token' => ['type' => 'string'],
'docker_cleanup_frequency' => ['type' => 'string'],
'docker_cleanup_threshold' => ['type' => 'integer'],
'server_id' => ['type' => 'integer'],
@@ -53,8 +54,85 @@ class ServerSetting extends Model
protected $casts = [
'force_docker_cleanup' => 'boolean',
'docker_cleanup_threshold' => 'integer',
+ 'sentinel_token' => 'encrypted',
+ 'is_reachable' => 'boolean',
+ 'is_usable' => 'boolean',
];
+ protected static function booted()
+ {
+ static::creating(function ($setting) {
+ try {
+ if (str($setting->sentinel_token)->isEmpty()) {
+ $setting->generateSentinelToken(save: false, ignoreEvent: true);
+ }
+ if (str($setting->sentinel_custom_url)->isEmpty()) {
+ $setting->generateSentinelUrl(save: false, ignoreEvent: true);
+ }
+ } catch (\Throwable $e) {
+ Log::error('Error creating server setting: '.$e->getMessage());
+ }
+ });
+ static::updated(function ($settings) {
+ if (
+ $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')
+ ) {
+ $settings->server->restartSentinel();
+ }
+ if ($settings->isDirty('is_reachable')) {
+ $settings->server->isReachableChanged();
+ }
+ });
+ }
+
+ public function generateSentinelToken(bool $save = true, bool $ignoreEvent = false)
+ {
+ $data = [
+ 'server_uuid' => $this->server->uuid,
+ ];
+ $token = json_encode($data);
+ $encrypted = encrypt($token);
+ $this->sentinel_token = $encrypted;
+ if ($save) {
+ if ($ignoreEvent) {
+ $this->saveQuietly();
+ } else {
+ $this->save();
+ }
+ }
+
+ return $token;
+ }
+
+ public function generateSentinelUrl(bool $save = true, bool $ignoreEvent = false)
+ {
+ $domain = null;
+ $settings = InstanceSettings::get();
+ if ($this->server->isLocalhost()) {
+ $domain = 'http://host.docker.internal:8000';
+ } elseif ($settings->fqdn) {
+ $domain = $settings->fqdn;
+ } 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;
+ if ($save) {
+ if ($ignoreEvent) {
+ $this->saveQuietly();
+ } else {
+ $this->save();
+ }
+ }
+
+ return $domain;
+ }
+
public function server()
{
return $this->belongsTo(Server::class);
diff --git a/app/Models/Service.php b/app/Models/Service.php
index 0036a9fda..0c9e081a1 100644
--- a/app/Models/Service.php
+++ b/app/Models/Service.php
@@ -133,6 +133,11 @@ class Service extends BaseModel
return $this->morphToMany(Tag::class, 'taggable');
}
+ public static function ownedByCurrentTeam()
+ {
+ return Service::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
+ }
+
public function getContainersToStop(): array
{
$containersToStop = [];
@@ -297,7 +302,7 @@ class Service extends BaseModel
'key' => 'CP_DISABLE_HTTPS',
'value' => data_get($disable_https, 'value'),
'rules' => 'required',
- 'customHelper' => "If you want to use https, set this to 0. Variable name: CP_DISABLE_HTTPS",
+ 'customHelper' => 'If you want to use https, set this to 0. Variable name: CP_DISABLE_HTTPS',
],
]);
}
@@ -366,7 +371,6 @@ class Service extends BaseModel
]);
}
$password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_LANGFUSE')->first();
- ray('password', $password);
if ($password) {
$data = $data->merge([
'Admin Password' => [
@@ -997,8 +1001,8 @@ class Service extends BaseModel
break;
case $image->contains('mysql'):
$userVariables = ['SERVICE_USER_MYSQL', 'SERVICE_USER_WORDPRESS', 'MYSQL_USER'];
- $passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS', 'MYSQL_PASSWORD','SERVICE_PASSWORD_64_MYSQL'];
- $rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT','SERVICE_PASSWORD_64_MYSQLROOT'];
+ $passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS', 'MYSQL_PASSWORD', 'SERVICE_PASSWORD_64_MYSQL'];
+ $rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT', 'SERVICE_PASSWORD_64_MYSQLROOT'];
$dbNameVariables = ['MYSQL_DATABASE'];
$mysql_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
$mysql_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
@@ -1096,7 +1100,6 @@ class Service extends BaseModel
}
$fields->put('MariaDB', $data->toArray());
break;
-
}
}
@@ -1108,7 +1111,6 @@ class Service extends BaseModel
foreach ($fields as $field) {
$key = data_get($field, 'key');
$value = data_get($field, 'value');
- ray($key, $value);
$found = $this->environment_variables()->where('key', $key)->first();
if ($found) {
$found->value = $value;
@@ -1232,7 +1234,6 @@ class Service extends BaseModel
public function environment_variables(): HasMany
{
-
return $this->hasMany(EnvironmentVariable::class)->orderByRaw("LOWER(key) LIKE LOWER('SERVICE%') DESC, LOWER(key) ASC");
}
@@ -1307,13 +1308,26 @@ class Service extends BaseModel
} else {
return collect([]);
}
-
}
public function networks()
{
- $networks = getTopLevelNetworks($this);
+ return getTopLevelNetworks($this);
+ }
- return $networks;
+ protected function isDeployable(): Attribute
+ {
+ return Attribute::make(
+ get: function () {
+ $envs = $this->environment_variables()->where('is_required', true)->get();
+ foreach ($envs as $env) {
+ if ($env->is_really_required) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ );
}
}
diff --git a/app/Models/ServiceApplication.php b/app/Models/ServiceApplication.php
index 0e79e1e2e..5cafc9042 100644
--- a/app/Models/ServiceApplication.php
+++ b/app/Models/ServiceApplication.php
@@ -19,6 +19,11 @@ class ServiceApplication extends BaseModel
$service->persistentStorages()->delete();
$service->fileStorages()->delete();
});
+ static::saving(function ($service) {
+ if ($service->isDirty('status')) {
+ $service->forceFill(['last_online_at' => now()]);
+ }
+ });
}
public function restart()
@@ -32,6 +37,11 @@ class ServiceApplication extends BaseModel
return ServiceApplication::whereRelation('service.environment.project.team', 'id', $teamId)->orderBy('name');
}
+ public static function ownedByCurrentTeam()
+ {
+ return ServiceApplication::whereRelation('service.environment.project.team', 'id', currentTeam()->id)->orderBy('name');
+ }
+
public function isRunning()
{
return str($this->status)->contains('running');
diff --git a/app/Models/ServiceDatabase.php b/app/Models/ServiceDatabase.php
index 927527118..5fdd52637 100644
--- a/app/Models/ServiceDatabase.php
+++ b/app/Models/ServiceDatabase.php
@@ -17,6 +17,21 @@ class ServiceDatabase extends BaseModel
$service->persistentStorages()->delete();
$service->fileStorages()->delete();
});
+ static::saving(function ($service) {
+ if ($service->isDirty('status')) {
+ $service->forceFill(['last_online_at' => now()]);
+ }
+ });
+ }
+
+ public static function ownedByCurrentTeamAPI(int $teamId)
+ {
+ return ServiceDatabase::whereRelation('service.environment.project.team', 'id', $teamId)->orderBy('name');
+ }
+
+ public static function ownedByCurrentTeam()
+ {
+ return ServiceDatabase::whereRelation('service.environment.project.team', 'id', currentTeam()->id)->orderBy('name');
}
public function restart()
diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php
index e4341b1b9..6d66c6854 100644
--- a/app/Models/StandaloneClickhouse.php
+++ b/app/Models/StandaloneClickhouse.php
@@ -38,6 +38,11 @@ class StandaloneClickhouse extends BaseModel
$database->environment_variables()->delete();
$database->tags()->detach();
});
+ static::saving(function ($database) {
+ if ($database->isDirty('status')) {
+ $database->forceFill(['last_online_at' => now()]);
+ }
+ });
}
protected function serverStatus(): Attribute
@@ -266,33 +271,48 @@ class StandaloneClickhouse extends BaseModel
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
- public function getMetrics(int $mins = 5)
+ public function getCpuMetrics(int $mins = 5)
{
$server = $this->destination->server;
$container_name = $this->uuid;
- if ($server->isMetricsEnabled()) {
- $from = now()->subMinutes($mins)->toIso8601ZuluString();
- $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
- if (str($metrics)->contains('error')) {
- $error = json_decode($metrics, true);
- $error = data_get($error, 'error', 'Something is not okay, are you okay?');
- if ($error == 'Unauthorized') {
- $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
- }
- throw new \Exception($error);
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
}
- $metrics = str($metrics)->explode("\n")->skip(1)->all();
- $parsedCollection = collect($metrics)->flatMap(function ($item) {
- return collect(explode("\n", trim($item)))->map(function ($line) {
- [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line));
- $cpu_usage_percent = number_format($cpu_usage_percent, 2);
-
- return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage];
- });
- });
-
- return $parsedCollection->toArray();
+ throw new \Exception($error);
}
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['percent']];
+ });
+
+ return $parsedCollection->toArray();
+ }
+
+ public function getMemoryMetrics(int $mins = 5)
+ {
+ $server = $this->destination->server;
+ $container_name = $this->uuid;
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
+ }
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['used']];
+ });
+
+ return $parsedCollection->toArray();
}
public function isBackupSolutionAvailable()
diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php
index 94ab2d745..f7d83f0a3 100644
--- a/app/Models/StandaloneDragonfly.php
+++ b/app/Models/StandaloneDragonfly.php
@@ -38,6 +38,11 @@ class StandaloneDragonfly extends BaseModel
$database->environment_variables()->delete();
$database->tags()->detach();
});
+ static::saving(function ($database) {
+ if ($database->isDirty('status')) {
+ $database->forceFill(['last_online_at' => now()]);
+ }
+ });
}
protected function serverStatus(): Attribute
@@ -266,33 +271,48 @@ class StandaloneDragonfly extends BaseModel
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
- public function getMetrics(int $mins = 5)
+ public function getCpuMetrics(int $mins = 5)
{
$server = $this->destination->server;
$container_name = $this->uuid;
- if ($server->isMetricsEnabled()) {
- $from = now()->subMinutes($mins)->toIso8601ZuluString();
- $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
- if (str($metrics)->contains('error')) {
- $error = json_decode($metrics, true);
- $error = data_get($error, 'error', 'Something is not okay, are you okay?');
- if ($error == 'Unauthorized') {
- $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
- }
- throw new \Exception($error);
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
}
- $metrics = str($metrics)->explode("\n")->skip(1)->all();
- $parsedCollection = collect($metrics)->flatMap(function ($item) {
- return collect(explode("\n", trim($item)))->map(function ($line) {
- [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line));
- $cpu_usage_percent = number_format($cpu_usage_percent, 2);
-
- return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage];
- });
- });
-
- return $parsedCollection->toArray();
+ throw new \Exception($error);
}
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['percent']];
+ });
+
+ return $parsedCollection->toArray();
+ }
+
+ public function getMemoryMetrics(int $mins = 5)
+ {
+ $server = $this->destination->server;
+ $container_name = $this->uuid;
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
+ }
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['used']];
+ });
+
+ return $parsedCollection->toArray();
}
public function isBackupSolutionAvailable()
diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php
index 335c8931c..083c743d9 100644
--- a/app/Models/StandaloneKeydb.php
+++ b/app/Models/StandaloneKeydb.php
@@ -38,6 +38,11 @@ class StandaloneKeydb extends BaseModel
$database->environment_variables()->delete();
$database->tags()->detach();
});
+ static::saving(function ($database) {
+ if ($database->isDirty('status')) {
+ $database->forceFill(['last_online_at' => now()]);
+ }
+ });
}
protected function serverStatus(): Attribute
@@ -266,33 +271,48 @@ class StandaloneKeydb extends BaseModel
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
- public function getMetrics(int $mins = 5)
+ public function getCpuMetrics(int $mins = 5)
{
$server = $this->destination->server;
$container_name = $this->uuid;
- if ($server->isMetricsEnabled()) {
- $from = now()->subMinutes($mins)->toIso8601ZuluString();
- $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
- if (str($metrics)->contains('error')) {
- $error = json_decode($metrics, true);
- $error = data_get($error, 'error', 'Something is not okay, are you okay?');
- if ($error == 'Unauthorized') {
- $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
- }
- throw new \Exception($error);
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
}
- $metrics = str($metrics)->explode("\n")->skip(1)->all();
- $parsedCollection = collect($metrics)->flatMap(function ($item) {
- return collect(explode("\n", trim($item)))->map(function ($line) {
- [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line));
- $cpu_usage_percent = number_format($cpu_usage_percent, 2);
-
- return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage];
- });
- });
-
- return $parsedCollection->toArray();
+ throw new \Exception($error);
}
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['percent']];
+ });
+
+ return $parsedCollection->toArray();
+ }
+
+ public function getMemoryMetrics(int $mins = 5)
+ {
+ $server = $this->destination->server;
+ $container_name = $this->uuid;
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
+ }
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['used']];
+ });
+
+ return $parsedCollection->toArray();
}
public function isBackupSolutionAvailable()
diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php
index c6c08dee5..833dad6c4 100644
--- a/app/Models/StandaloneMariadb.php
+++ b/app/Models/StandaloneMariadb.php
@@ -38,6 +38,11 @@ class StandaloneMariadb extends BaseModel
$database->environment_variables()->delete();
$database->tags()->detach();
});
+ static::saving(function ($database) {
+ if ($database->isDirty('status')) {
+ $database->forceFill(['last_online_at' => now()]);
+ }
+ });
}
protected function serverStatus(): Attribute
@@ -266,33 +271,48 @@ class StandaloneMariadb extends BaseModel
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
- public function getMetrics(int $mins = 5)
+ public function getCpuMetrics(int $mins = 5)
{
$server = $this->destination->server;
$container_name = $this->uuid;
- if ($server->isMetricsEnabled()) {
- $from = now()->subMinutes($mins)->toIso8601ZuluString();
- $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
- if (str($metrics)->contains('error')) {
- $error = json_decode($metrics, true);
- $error = data_get($error, 'error', 'Something is not okay, are you okay?');
- if ($error == 'Unauthorized') {
- $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
- }
- throw new \Exception($error);
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
}
- $metrics = str($metrics)->explode("\n")->skip(1)->all();
- $parsedCollection = collect($metrics)->flatMap(function ($item) {
- return collect(explode("\n", trim($item)))->map(function ($line) {
- [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line));
- $cpu_usage_percent = number_format($cpu_usage_percent, 2);
-
- return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage];
- });
- });
-
- return $parsedCollection->toArray();
+ throw new \Exception($error);
}
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['percent']];
+ });
+
+ return $parsedCollection->toArray();
+ }
+
+ public function getMemoryMetrics(int $mins = 5)
+ {
+ $server = $this->destination->server;
+ $container_name = $this->uuid;
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
+ }
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['used']];
+ });
+
+ return $parsedCollection->toArray();
}
public function isBackupSolutionAvailable()
diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php
index 99893b1d1..dd8893180 100644
--- a/app/Models/StandaloneMongodb.php
+++ b/app/Models/StandaloneMongodb.php
@@ -42,6 +42,11 @@ class StandaloneMongodb extends BaseModel
$database->environment_variables()->delete();
$database->tags()->detach();
});
+ static::saving(function ($database) {
+ if ($database->isDirty('status')) {
+ $database->forceFill(['last_online_at' => now()]);
+ }
+ });
}
protected function serverStatus(): Attribute
@@ -286,33 +291,48 @@ class StandaloneMongodb extends BaseModel
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
- public function getMetrics(int $mins = 5)
+ public function getCpuMetrics(int $mins = 5)
{
$server = $this->destination->server;
$container_name = $this->uuid;
- if ($server->isMetricsEnabled()) {
- $from = now()->subMinutes($mins)->toIso8601ZuluString();
- $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
- if (str($metrics)->contains('error')) {
- $error = json_decode($metrics, true);
- $error = data_get($error, 'error', 'Something is not okay, are you okay?');
- if ($error == 'Unauthorized') {
- $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
- }
- throw new \Exception($error);
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
}
- $metrics = str($metrics)->explode("\n")->skip(1)->all();
- $parsedCollection = collect($metrics)->flatMap(function ($item) {
- return collect(explode("\n", trim($item)))->map(function ($line) {
- [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line));
- $cpu_usage_percent = number_format($cpu_usage_percent, 2);
-
- return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage];
- });
- });
-
- return $parsedCollection->toArray();
+ throw new \Exception($error);
}
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['percent']];
+ });
+
+ return $parsedCollection->toArray();
+ }
+
+ public function getMemoryMetrics(int $mins = 5)
+ {
+ $server = $this->destination->server;
+ $container_name = $this->uuid;
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
+ }
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['used']];
+ });
+
+ return $parsedCollection->toArray();
}
public function isBackupSolutionAvailable()
diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php
index f2a5b5c14..710fea1bc 100644
--- a/app/Models/StandaloneMysql.php
+++ b/app/Models/StandaloneMysql.php
@@ -39,6 +39,11 @@ class StandaloneMysql extends BaseModel
$database->environment_variables()->delete();
$database->tags()->detach();
});
+ static::saving(function ($database) {
+ if ($database->isDirty('status')) {
+ $database->forceFill(['last_online_at' => now()]);
+ }
+ });
}
protected function serverStatus(): Attribute
@@ -267,33 +272,48 @@ class StandaloneMysql extends BaseModel
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
- public function getMetrics(int $mins = 5)
+ public function getCpuMetrics(int $mins = 5)
{
$server = $this->destination->server;
$container_name = $this->uuid;
- if ($server->isMetricsEnabled()) {
- $from = now()->subMinutes($mins)->toIso8601ZuluString();
- $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
- if (str($metrics)->contains('error')) {
- $error = json_decode($metrics, true);
- $error = data_get($error, 'error', 'Something is not okay, are you okay?');
- if ($error == 'Unauthorized') {
- $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
- }
- throw new \Exception($error);
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
}
- $metrics = str($metrics)->explode("\n")->skip(1)->all();
- $parsedCollection = collect($metrics)->flatMap(function ($item) {
- return collect(explode("\n", trim($item)))->map(function ($line) {
- [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line));
- $cpu_usage_percent = number_format($cpu_usage_percent, 2);
-
- return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage];
- });
- });
-
- return $parsedCollection->toArray();
+ throw new \Exception($error);
}
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['percent']];
+ });
+
+ return $parsedCollection->toArray();
+ }
+
+ public function getMemoryMetrics(int $mins = 5)
+ {
+ $server = $this->destination->server;
+ $container_name = $this->uuid;
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
+ }
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['used']];
+ });
+
+ return $parsedCollection->toArray();
}
public function isBackupSolutionAvailable()
diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php
index 1b18a5ca7..4a457a6cf 100644
--- a/app/Models/StandalonePostgresql.php
+++ b/app/Models/StandalonePostgresql.php
@@ -39,6 +39,11 @@ class StandalonePostgresql extends BaseModel
$database->environment_variables()->delete();
$database->tags()->detach();
});
+ static::saving(function ($database) {
+ if ($database->isDirty('status')) {
+ $database->forceFill(['last_online_at' => now()]);
+ }
+ });
}
public function workdir()
@@ -71,7 +76,6 @@ class StandalonePostgresql extends BaseModel
}
$server = data_get($this, 'destination.server');
foreach ($persistentStorages as $storage) {
- ray('Deleting volume: '.$storage->name);
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
@@ -268,37 +272,52 @@ class StandalonePostgresql extends BaseModel
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
- public function getMetrics(int $mins = 5)
- {
- $server = $this->destination->server;
- $container_name = $this->uuid;
- if ($server->isMetricsEnabled()) {
- $from = now()->subMinutes($mins)->toIso8601ZuluString();
- $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
- if (str($metrics)->contains('error')) {
- $error = json_decode($metrics, true);
- $error = data_get($error, 'error', 'Something is not okay, are you okay?');
- if ($error == 'Unauthorized') {
- $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
- }
- throw new \Exception($error);
- }
- $metrics = str($metrics)->explode("\n")->skip(1)->all();
- $parsedCollection = collect($metrics)->flatMap(function ($item) {
- return collect(explode("\n", trim($item)))->map(function ($line) {
- [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line));
- $cpu_usage_percent = number_format($cpu_usage_percent, 2);
-
- return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage];
- });
- });
-
- return $parsedCollection->toArray();
- }
- }
-
public function isBackupSolutionAvailable()
{
return true;
}
+
+ public function getCpuMetrics(int $mins = 5)
+ {
+ $server = $this->destination->server;
+ $container_name = $this->uuid;
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
+ }
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['percent']];
+ });
+
+ return $parsedCollection->toArray();
+ }
+
+ public function getMemoryMetrics(int $mins = 5)
+ {
+ $server = $this->destination->server;
+ $container_name = $this->uuid;
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
+ }
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['used']];
+ });
+
+ return $parsedCollection->toArray();
+ }
}
diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php
index a5868e243..826bb951c 100644
--- a/app/Models/StandaloneRedis.php
+++ b/app/Models/StandaloneRedis.php
@@ -34,6 +34,11 @@ class StandaloneRedis extends BaseModel
$database->environment_variables()->delete();
$database->tags()->detach();
});
+ static::saving(function ($database) {
+ if ($database->isDirty('status')) {
+ $database->forceFill(['last_online_at' => now()]);
+ }
+ });
}
protected function serverStatus(): Attribute
@@ -210,7 +215,12 @@ class StandaloneRedis extends BaseModel
protected function internalDbUrl(): Attribute
{
return new Attribute(
- get: fn () => "redis://:{$this->redis_password}@{$this->uuid}:6379/0",
+ get: function () {
+ $redis_version = $this->getRedisVersion();
+ $username_part = version_compare($redis_version, '6.0', '>=') ? "{$this->redis_username}:" : '';
+
+ return "redis://{$username_part}{$this->redis_password}@{$this->uuid}:6379/0";
+ }
);
}
@@ -219,7 +229,10 @@ class StandaloneRedis extends BaseModel
return new Attribute(
get: function () {
if ($this->is_public && $this->public_port) {
- return "redis://:{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
+ $redis_version = $this->getRedisVersion();
+ $username_part = version_compare($redis_version, '6.0', '>=') ? "{$this->redis_username}:" : '';
+
+ return "redis://{$username_part}{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
}
return null;
@@ -227,6 +240,13 @@ class StandaloneRedis extends BaseModel
);
}
+ public function getRedisVersion()
+ {
+ $image_parts = explode(':', $this->image);
+
+ return $image_parts[1] ?? '0.0';
+ }
+
public function environment()
{
return $this->belongsTo(Environment::class);
@@ -262,37 +282,81 @@ class StandaloneRedis extends BaseModel
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
- public function getMetrics(int $mins = 5)
+ public function getCpuMetrics(int $mins = 5)
{
$server = $this->destination->server;
$container_name = $this->uuid;
- if ($server->isMetricsEnabled()) {
- $from = now()->subMinutes($mins)->toIso8601ZuluString();
- $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
- if (str($metrics)->contains('error')) {
- $error = json_decode($metrics, true);
- $error = data_get($error, 'error', 'Something is not okay, are you okay?');
- if ($error == 'Unauthorized') {
- $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
- }
- throw new \Exception($error);
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
}
- $metrics = str($metrics)->explode("\n")->skip(1)->all();
- $parsedCollection = collect($metrics)->flatMap(function ($item) {
- return collect(explode("\n", trim($item)))->map(function ($line) {
- [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line));
- $cpu_usage_percent = number_format($cpu_usage_percent, 2);
-
- return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage];
- });
- });
-
- return $parsedCollection->toArray();
+ throw new \Exception($error);
}
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['percent']];
+ });
+
+ return $parsedCollection->toArray();
+ }
+
+ public function getMemoryMetrics(int $mins = 5)
+ {
+ $server = $this->destination->server;
+ $container_name = $this->uuid;
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
+ }
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['used']];
+ });
+
+ return $parsedCollection->toArray();
}
public function isBackupSolutionAvailable()
{
return false;
}
+
+ public function redisPassword(): Attribute
+ {
+ return new Attribute(
+ get: function () {
+ $password = $this->runtime_environment_variables()->where('key', 'REDIS_PASSWORD')->first();
+ if (! $password) {
+ return null;
+ }
+
+ return $password->value;
+ },
+
+ );
+ }
+
+ public function redisUsername(): Attribute
+ {
+ return new Attribute(
+ get: function () {
+ $username = $this->runtime_environment_variables()->where('key', 'REDIS_USERNAME')->first();
+ if (! $username) {
+ return null;
+ }
+
+ return $username->value;
+ }
+ );
+ }
}
diff --git a/app/Models/Team.php b/app/Models/Team.php
index 3f8e97bc5..db485054b 100644
--- a/app/Models/Team.php
+++ b/app/Models/Team.php
@@ -34,6 +34,7 @@ use OpenApi\Attributes as OA;
'smtp_notifications_status_changes' => ['type' => 'boolean', 'description' => 'Whether to send status change notifications via SMTP.'],
'smtp_notifications_scheduled_tasks' => ['type' => 'boolean', 'description' => 'Whether to send scheduled task notifications via SMTP.'],
'smtp_notifications_database_backups' => ['type' => 'boolean', 'description' => 'Whether to send database backup notifications via SMTP.'],
+ 'smtp_notifications_server_disk_usage' => ['type' => 'boolean', 'description' => 'Whether to send server disk usage notifications via SMTP.'],
'discord_enabled' => ['type' => 'boolean', 'description' => 'Whether Discord is enabled or not.'],
'discord_webhook_url' => ['type' => 'string', 'description' => 'The Discord webhook URL.'],
'discord_notifications_test' => ['type' => 'boolean', 'description' => 'Whether to send test notifications via Discord.'],
@@ -41,6 +42,7 @@ use OpenApi\Attributes as OA;
'discord_notifications_status_changes' => ['type' => 'boolean', 'description' => 'Whether to send status change notifications via Discord.'],
'discord_notifications_database_backups' => ['type' => 'boolean', 'description' => 'Whether to send database backup notifications via Discord.'],
'discord_notifications_scheduled_tasks' => ['type' => 'boolean', 'description' => 'Whether to send scheduled task notifications via Discord.'],
+ 'discord_notifications_server_disk_usage' => ['type' => 'boolean', 'description' => 'Whether to send server disk usage notifications via Discord.'],
'show_boarding' => ['type' => 'boolean', 'description' => 'Whether to show the boarding screen or not.'],
'resend_enabled' => ['type' => 'boolean', 'description' => 'Whether to enable resending or not.'],
'resend_api_key' => ['type' => 'string', 'description' => 'The resending API key.'],
@@ -56,6 +58,7 @@ use OpenApi\Attributes as OA;
'telegram_notifications_deployments_message_thread_id' => ['type' => 'string', 'description' => 'The Telegram deployment message thread ID.'],
'telegram_notifications_status_changes_message_thread_id' => ['type' => 'string', 'description' => 'The Telegram status change message thread ID.'],
'telegram_notifications_database_backups_message_thread_id' => ['type' => 'string', 'description' => 'The Telegram database backup message thread ID.'],
+
'custom_server_limit' => ['type' => 'string', 'description' => 'The custom server limit.'],
'telegram_notifications_scheduled_tasks' => ['type' => 'boolean', 'description' => 'Whether to send scheduled task notifications via Telegram.'],
'telegram_notifications_scheduled_tasks_thread_id' => ['type' => 'string', 'description' => 'The Telegram scheduled task message thread ID.'],
@@ -90,27 +93,22 @@ class Team extends Model implements SendsDiscord, SendsEmail
static::deleting(function ($team) {
$keys = $team->privateKeys;
foreach ($keys as $key) {
- ray('Deleting key: '.$key->name);
$key->delete();
}
$sources = $team->sources();
foreach ($sources as $source) {
- ray('Deleting source: '.$source->name);
$source->delete();
}
$tags = Tag::whereTeamId($team->id)->get();
foreach ($tags as $tag) {
- ray('Deleting tag: '.$tag->name);
$tag->delete();
}
$shared_variables = $team->environment_variables();
foreach ($shared_variables as $shared_variable) {
- ray('Deleting team shared variable: '.$shared_variable->name);
$shared_variable->delete();
}
$s3s = $team->s3s;
foreach ($s3s as $s3) {
- ray('Deleting s3: '.$s3->name);
$s3->delete();
}
});
@@ -133,9 +131,7 @@ class Team extends Model implements SendsDiscord, SendsEmail
{
$recipients = data_get($notification, 'emails', null);
if (is_null($recipients)) {
- $recipients = $this->members()->pluck('email')->toArray();
-
- return $recipients;
+ return $this->members()->pluck('email')->toArray();
}
return explode(',', $recipients);
@@ -164,8 +160,12 @@ class Team extends Model implements SendsDiscord, SendsEmail
if (currentTeam()->id === 0 && isDev()) {
return 9999999;
}
+ $team = Team::find(currentTeam()->id);
+ if (! $team) {
+ return 0;
+ }
- return Team::find(currentTeam()->id)->limits['serverLimit'];
+ return data_get($team, 'limits', 0);
}
public function limits(): Attribute
@@ -187,9 +187,8 @@ class Team extends Model implements SendsDiscord, SendsEmail
} else {
$serverLimit = config('constants.limits.server')[strtolower($subscription)];
}
- $sharedEmailEnabled = config('constants.limits.email')[strtolower($subscription)];
- return ['serverLimit' => $serverLimit, 'sharedEmailEnabled' => $sharedEmailEnabled];
+ return $serverLimit ?? 2;
}
);
@@ -249,9 +248,8 @@ class Team extends Model implements SendsDiscord, SendsEmail
$sources = collect([]);
$github_apps = $this->hasMany(GithubApp::class)->whereisPublic(false)->get();
$gitlab_apps = $this->hasMany(GitlabApp::class)->whereisPublic(false)->get();
- $sources = $sources->merge($github_apps)->merge($gitlab_apps);
- return $sources;
+ return $sources->merge($github_apps)->merge($gitlab_apps);
}
public function s3s()
diff --git a/app/Models/TeamInvitation.php b/app/Models/TeamInvitation.php
index c202710e2..bc1a90d58 100644
--- a/app/Models/TeamInvitation.php
+++ b/app/Models/TeamInvitation.php
@@ -20,11 +20,16 @@ class TeamInvitation extends Model
return $this->belongsTo(Team::class);
}
+ public static function ownedByCurrentTeam()
+ {
+ return TeamInvitation::whereTeamId(currentTeam()->id);
+ }
+
public function isValid()
{
$createdAt = $this->created_at;
- $diff = $createdAt->diffInMinutes(now());
- if ($diff <= config('constants.invitation.link.expiration')) {
+ $diff = $createdAt->diffInDays(now());
+ if ($diff <= config('constants.invitation.link.expiration_days')) {
return true;
} else {
$this->delete();
diff --git a/app/Models/User.php b/app/Models/User.php
index ecc4ef6b6..25fb33d66 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -10,6 +10,7 @@ use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Carbon;
+use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\URL;
@@ -158,7 +159,7 @@ class User extends Authenticatable implements SendsEmail
public function isAdminFromSession()
{
- if (auth()->user()->id === 0) {
+ if (Auth::id() === 0) {
return true;
}
$teams = $this->teams()->get();
@@ -178,9 +179,9 @@ class User extends Authenticatable implements SendsEmail
public function isInstanceAdmin()
{
- $found_root_team = auth()->user()->teams->filter(function ($team) {
+ $found_root_team = Auth::user()->teams->filter(function ($team) {
if ($team->id == 0) {
- if (! auth()->user()->isAdmin()) {
+ if (! Auth::user()->isAdmin()) {
return false;
}
@@ -195,9 +196,9 @@ class User extends Authenticatable implements SendsEmail
public function currentTeam()
{
- return Cache::remember('team:'.auth()->user()->id, 3600, function () {
- if (is_null(data_get(session('currentTeam'), 'id')) && auth()->user()->teams->count() > 0) {
- return auth()->user()->teams[0];
+ return Cache::remember('team:'.Auth::id(), 3600, function () {
+ if (is_null(data_get(session('currentTeam'), 'id')) && Auth::user()->teams->count() > 0) {
+ return Auth::user()->teams[0];
}
return Team::find(session('currentTeam')->id);
@@ -206,7 +207,7 @@ class User extends Authenticatable implements SendsEmail
public function otherTeams()
{
- return auth()->user()->teams->filter(function ($team) {
+ return Auth::user()->teams->filter(function ($team) {
return $team->id != currentTeam()->id;
});
}
@@ -216,7 +217,7 @@ class User extends Authenticatable implements SendsEmail
if (data_get($this, 'pivot')) {
return $this->pivot->role;
}
- $user = auth()->user()->teams->where('id', currentTeam()->id)->first();
+ $user = Auth::user()->teams->where('id', currentTeam()->id)->first();
return data_get($user, 'pivot.role');
}
diff --git a/app/Notifications/Application/DeploymentFailed.php b/app/Notifications/Application/DeploymentFailed.php
index 1809da368..242980e00 100644
--- a/app/Notifications/Application/DeploymentFailed.php
+++ b/app/Notifications/Application/DeploymentFailed.php
@@ -4,6 +4,7 @@ namespace App\Notifications\Application;
use App\Models\Application;
use App\Models\ApplicationPreview;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
@@ -72,14 +73,42 @@ class DeploymentFailed extends Notification implements ShouldQueue
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
if ($this->preview) {
- $message = 'Coolify: Pull request #'.$this->preview->pull_request_id.' of '.$this->application_name.' ('.$this->preview->fqdn.') deployment failed: ';
- $message .= '[View Deployment Logs]('.$this->deployment_url.')';
+ $message = new DiscordMessage(
+ title: ':cross_mark: Deployment failed',
+ description: 'Pull request: '.$this->preview->pull_request_id,
+ color: DiscordMessage::errorColor(),
+ isCritical: true,
+ );
+
+ $message->addField('Project', data_get($this->application, 'environment.project.name'), true);
+ $message->addField('Environment', $this->environment_name, true);
+ $message->addField('Name', $this->application_name, true);
+
+ $message->addField('Deployment Logs', '[Link]('.$this->deployment_url.')');
+ if ($this->fqdn) {
+ $message->addField('Domain', $this->fqdn, true);
+ }
} else {
- $message = 'Coolify: Deployment failed of '.$this->application_name.' ('.$this->fqdn.'): ';
- $message .= '[View Deployment Logs]('.$this->deployment_url.')';
+ if ($this->fqdn) {
+ $description = '[Open application]('.$this->fqdn.')';
+ } else {
+ $description = '';
+ }
+ $message = new DiscordMessage(
+ title: ':cross_mark: Deployment failed',
+ description: $description,
+ color: DiscordMessage::errorColor(),
+ isCritical: true,
+ );
+
+ $message->addField('Project', data_get($this->application, 'environment.project.name'), true);
+ $message->addField('Environment', $this->environment_name, true);
+ $message->addField('Name', $this->application_name, true);
+
+ $message->addField('Deployment Logs', '[Link]('.$this->deployment_url.')');
}
return $message;
diff --git a/app/Notifications/Application/DeploymentSuccess.php b/app/Notifications/Application/DeploymentSuccess.php
index 5085065c2..946a622ca 100644
--- a/app/Notifications/Application/DeploymentSuccess.php
+++ b/app/Notifications/Application/DeploymentSuccess.php
@@ -4,6 +4,7 @@ namespace App\Notifications\Application;
use App\Models\Application;
use App\Models\ApplicationPreview;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
@@ -51,7 +52,7 @@ class DeploymentSuccess extends Notification implements ShouldQueue
$channels = setNotificationChannels($notifiable, 'deployments');
if (isCloud()) {
// TODO: Make batch notifications work with email
- $channels = array_diff($channels, ['App\Notifications\Channels\EmailChannel']);
+ $channels = array_diff($channels, [\App\Notifications\Channels\EmailChannel::class]);
}
return $channels;
@@ -78,24 +79,39 @@ class DeploymentSuccess extends Notification implements ShouldQueue
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
if ($this->preview) {
- $message = 'Coolify: New PR'.$this->preview->pull_request_id.' version successfully deployed of '.$this->application_name.'
+ $message = new DiscordMessage(
+ title: ':white_check_mark: Preview deployment successful',
+ description: 'Pull request: '.$this->preview->pull_request_id,
+ color: DiscordMessage::successColor(),
+ );
-';
if ($this->preview->fqdn) {
- $message .= '[Open Application]('.$this->preview->fqdn.') | ';
+ $message->addField('Application', '[Link]('.$this->preview->fqdn.')');
}
- $message .= '[Deployment logs]('.$this->deployment_url.')';
- } else {
- $message = 'Coolify: New version successfully deployed of '.$this->application_name.'
-';
+ $message->addField('Project', data_get($this->application, 'environment.project.name'), true);
+ $message->addField('Environment', $this->environment_name, true);
+ $message->addField('Name', $this->application_name, true);
+ $message->addField('Deployment logs', '[Link]('.$this->deployment_url.')');
+ } else {
if ($this->fqdn) {
- $message .= '[Open Application]('.$this->fqdn.') | ';
+ $description = '[Open application]('.$this->fqdn.')';
+ } else {
+ $description = '';
}
- $message .= '[Deployment logs]('.$this->deployment_url.')';
+ $message = new DiscordMessage(
+ title: ':white_check_mark: New version successfully deployed',
+ description: $description,
+ color: DiscordMessage::successColor(),
+ );
+ $message->addField('Project', data_get($this->application, 'environment.project.name'), true);
+ $message->addField('Environment', $this->environment_name, true);
+ $message->addField('Name', $this->application_name, true);
+
+ $message->addField('Deployment logs', '[Link]('.$this->deployment_url.')');
}
return $message;
diff --git a/app/Notifications/Application/StatusChanged.php b/app/Notifications/Application/StatusChanged.php
index 53ed8a589..852c6b526 100644
--- a/app/Notifications/Application/StatusChanged.php
+++ b/app/Notifications/Application/StatusChanged.php
@@ -3,6 +3,7 @@
namespace App\Notifications\Application;
use App\Models\Application;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
@@ -55,14 +56,14 @@ class StatusChanged extends Notification implements ShouldQueue
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
- $message = 'Coolify: '.$this->resource_name.' has been stopped.
-
-';
- $message .= '[Open Application in Coolify]('.$this->resource_url.')';
-
- return $message;
+ return new DiscordMessage(
+ title: ':cross_mark: Application stopped',
+ description: '[Open Application in Coolify]('.$this->resource_url.')',
+ color: DiscordMessage::errorColor(),
+ isCritical: true,
+ );
}
public function toTelegram(): array
diff --git a/app/Notifications/Channels/DiscordChannel.php b/app/Notifications/Channels/DiscordChannel.php
index f1706f138..86276fec9 100644
--- a/app/Notifications/Channels/DiscordChannel.php
+++ b/app/Notifications/Channels/DiscordChannel.php
@@ -12,11 +12,11 @@ class DiscordChannel
*/
public function send(SendsDiscord $notifiable, Notification $notification): void
{
- $message = $notification->toDiscord($notifiable);
+ $message = $notification->toDiscord();
$webhookUrl = $notifiable->routeNotificationForDiscord();
if (! $webhookUrl) {
return;
}
- dispatch(new SendMessageToDiscordJob($message, $webhookUrl));
+ dispatch(new SendMessageToDiscordJob($message, $webhookUrl))->onQueue('high');
}
}
diff --git a/app/Notifications/Channels/EmailChannel.php b/app/Notifications/Channels/EmailChannel.php
index 413d3de53..af9af978d 100644
--- a/app/Notifications/Channels/EmailChannel.php
+++ b/app/Notifications/Channels/EmailChannel.php
@@ -32,7 +32,6 @@ class EmailChannel
if ($error === 'No email settings found.') {
throw $e;
}
- ray($e->getMessage());
$message = "EmailChannel error: {$e->getMessage()}. Failed to send email to:";
if (isset($recipients)) {
$message .= implode(', ', $recipients);
diff --git a/app/Notifications/Channels/TelegramChannel.php b/app/Notifications/Channels/TelegramChannel.php
index b1a607651..b3d4e384b 100644
--- a/app/Notifications/Channels/TelegramChannel.php
+++ b/app/Notifications/Channels/TelegramChannel.php
@@ -18,29 +18,29 @@ class TelegramChannel
$topicsInstance = get_class($notification);
switch ($topicsInstance) {
- case 'App\Notifications\Test':
+ case \App\Notifications\Test::class:
$topicId = data_get($notifiable, 'telegram_notifications_test_message_thread_id');
break;
- case 'App\Notifications\Application\StatusChanged':
- case 'App\Notifications\Container\ContainerRestarted':
- case 'App\Notifications\Container\ContainerStopped':
+ case \App\Notifications\Application\StatusChanged::class:
+ case \App\Notifications\Container\ContainerRestarted::class:
+ case \App\Notifications\Container\ContainerStopped::class:
$topicId = data_get($notifiable, 'telegram_notifications_status_changes_message_thread_id');
break;
- case 'App\Notifications\Application\DeploymentSuccess':
- case 'App\Notifications\Application\DeploymentFailed':
+ case \App\Notifications\Application\DeploymentSuccess::class:
+ case \App\Notifications\Application\DeploymentFailed::class:
$topicId = data_get($notifiable, 'telegram_notifications_deployments_message_thread_id');
break;
- case 'App\Notifications\Database\BackupSuccess':
- case 'App\Notifications\Database\BackupFailed':
+ case \App\Notifications\Database\BackupSuccess::class:
+ case \App\Notifications\Database\BackupFailed::class:
$topicId = data_get($notifiable, 'telegram_notifications_database_backups_message_thread_id');
break;
- case 'App\Notifications\ScheduledTask\TaskFailed':
+ case \App\Notifications\ScheduledTask\TaskFailed::class:
$topicId = data_get($notifiable, 'telegram_notifications_scheduled_tasks_thread_id');
break;
}
if (! $telegramToken || ! $chatId || ! $message) {
return;
}
- dispatch(new SendMessageToTelegramJob($message, $buttons, $telegramToken, $chatId, $topicId));
+ dispatch(new SendMessageToTelegramJob($message, $buttons, $telegramToken, $chatId, $topicId))->onQueue('high');
}
}
diff --git a/app/Notifications/Container/ContainerRestarted.php b/app/Notifications/Container/ContainerRestarted.php
index 23f6de264..182a1f5fc 100644
--- a/app/Notifications/Container/ContainerRestarted.php
+++ b/app/Notifications/Container/ContainerRestarted.php
@@ -3,6 +3,7 @@
namespace App\Notifications\Container;
use App\Models\Server;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
@@ -34,9 +35,17 @@ class ContainerRestarted extends Notification implements ShouldQueue
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
- $message = "Coolify: A resource ({$this->name}) has been restarted automatically on {$this->server->name}";
+ $message = new DiscordMessage(
+ title: ':warning: Resource restarted',
+ description: "{$this->name} has been restarted automatically on {$this->server->name}.",
+ color: DiscordMessage::infoColor(),
+ );
+
+ if ($this->url) {
+ $message->addField('Resource', '[Link]('.$this->url.')');
+ }
return $message;
}
diff --git a/app/Notifications/Container/ContainerStopped.php b/app/Notifications/Container/ContainerStopped.php
index bcf5e67a5..33a55c65a 100644
--- a/app/Notifications/Container/ContainerStopped.php
+++ b/app/Notifications/Container/ContainerStopped.php
@@ -3,6 +3,7 @@
namespace App\Notifications\Container;
use App\Models\Server;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
@@ -34,9 +35,17 @@ class ContainerStopped extends Notification implements ShouldQueue
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
- $message = "Coolify: A resource ($this->name) has been stopped unexpectedly on {$this->server->name}";
+ $message = new DiscordMessage(
+ title: ':cross_mark: Resource stopped',
+ description: "{$this->name} has been stopped unexpectedly on {$this->server->name}.",
+ color: DiscordMessage::errorColor(),
+ );
+
+ if ($this->url) {
+ $message->addField('Resource', '[Link]('.$this->url.')');
+ }
return $message;
}
diff --git a/app/Notifications/Database/BackupFailed.php b/app/Notifications/Database/BackupFailed.php
index 77024c05b..8e2733339 100644
--- a/app/Notifications/Database/BackupFailed.php
+++ b/app/Notifications/Database/BackupFailed.php
@@ -3,6 +3,7 @@
namespace App\Notifications\Database;
use App\Models\ScheduledDatabaseBackup;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
@@ -45,9 +46,19 @@ class BackupFailed extends Notification implements ShouldQueue
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
- return "Coolify: Database backup for {$this->name} (db:{$this->database_name}) with frequency of {$this->frequency} was FAILED.\n\nReason:\n{$this->output}";
+ $message = new DiscordMessage(
+ title: ':cross_mark: Database backup failed',
+ description: "Database backup for {$this->name} (db:{$this->database_name}) has FAILED.",
+ color: DiscordMessage::errorColor(),
+ isCritical: true,
+ );
+
+ $message->addField('Frequency', $this->frequency, true);
+ $message->addField('Output', $this->output);
+
+ return $message;
}
public function toTelegram(): array
diff --git a/app/Notifications/Database/BackupSuccess.php b/app/Notifications/Database/BackupSuccess.php
index f8dc6eb56..5128c8ed6 100644
--- a/app/Notifications/Database/BackupSuccess.php
+++ b/app/Notifications/Database/BackupSuccess.php
@@ -3,6 +3,7 @@
namespace App\Notifications\Database;
use App\Models\ScheduledDatabaseBackup;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
@@ -44,15 +45,22 @@ class BackupSuccess extends Notification implements ShouldQueue
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
- return "Coolify: Database backup for {$this->name} (db:{$this->database_name}) with frequency of {$this->frequency} was successful.";
+ $message = new DiscordMessage(
+ title: ':white_check_mark: Database backup successful',
+ description: "Database backup for {$this->name} (db:{$this->database_name}) was successful.",
+ color: DiscordMessage::successColor(),
+ );
+
+ $message->addField('Frequency', $this->frequency, true);
+
+ return $message;
}
public function toTelegram(): array
{
$message = "Coolify: Database backup for {$this->name} (db:{$this->database_name}) with frequency of {$this->frequency} was successful.";
- ray($message);
return [
'message' => $message,
diff --git a/app/Notifications/Database/DailyBackup.php b/app/Notifications/Database/DailyBackup.php
deleted file mode 100644
index a51ac6283..000000000
--- a/app/Notifications/Database/DailyBackup.php
+++ /dev/null
@@ -1,50 +0,0 @@
-subject('Coolify: Daily backup statuses');
- $mail->view('emails.daily-backup', [
- 'databases' => $this->databases,
- ]);
-
- return $mail;
- }
-
- public function toDiscord(): string
- {
- return 'Coolify: Daily backup statuses';
- }
-
- public function toTelegram(): array
- {
- $message = 'Coolify: Daily backup statuses';
-
- return [
- 'message' => $message,
- ];
- }
-}
diff --git a/app/Notifications/Dto/DiscordMessage.php b/app/Notifications/Dto/DiscordMessage.php
new file mode 100644
index 000000000..856753dca
--- /dev/null
+++ b/app/Notifications/Dto/DiscordMessage.php
@@ -0,0 +1,83 @@
+fields[] = [
+ 'name' => $name,
+ 'value' => $value,
+ 'inline' => $inline,
+ ];
+
+ return $this;
+ }
+
+ public function toPayload(): array
+ {
+ $footerText = 'Coolify v'.config('version');
+ if (isCloud()) {
+ $footerText = 'Coolify Cloud';
+ }
+ $payload = [
+ 'embeds' => [
+ [
+ 'title' => $this->title,
+ 'description' => $this->description,
+ 'color' => $this->color,
+ 'fields' => $this->addTimestampToFields($this->fields),
+ 'footer' => [
+ 'text' => $footerText,
+ ],
+ ],
+ ],
+ ];
+ if ($this->isCritical) {
+ $payload['content'] = '@here';
+ }
+
+ return $payload;
+ }
+
+ private function addTimestampToFields(array $fields): array
+ {
+ $fields[] = [
+ 'name' => 'Time',
+ 'value' => 'timestamp.':R>',
+ 'inline' => true,
+ ];
+
+ return $fields;
+ }
+}
diff --git a/app/Notifications/Internal/GeneralNotification.php b/app/Notifications/Internal/GeneralNotification.php
index 1d4d648c8..48e7d8340 100644
--- a/app/Notifications/Internal/GeneralNotification.php
+++ b/app/Notifications/Internal/GeneralNotification.php
@@ -4,6 +4,7 @@ namespace App\Notifications\Internal;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\TelegramChannel;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;
@@ -32,9 +33,13 @@ class GeneralNotification extends Notification implements ShouldQueue
return $channels;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
- return $this->message;
+ return new DiscordMessage(
+ title: 'Coolify: General Notification',
+ description: $this->message,
+ color: DiscordMessage::infoColor(),
+ );
}
public function toTelegram(): array
diff --git a/app/Notifications/ScheduledTask/TaskFailed.php b/app/Notifications/ScheduledTask/TaskFailed.php
index 479cc1aa1..c3501a8eb 100644
--- a/app/Notifications/ScheduledTask/TaskFailed.php
+++ b/app/Notifications/ScheduledTask/TaskFailed.php
@@ -3,6 +3,7 @@
namespace App\Notifications\ScheduledTask;
use App\Models\ScheduledTask;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
@@ -29,7 +30,6 @@ class TaskFailed extends Notification implements ShouldQueue
public function via(object $notifiable): array
{
-
return setNotificationChannels($notifiable, 'scheduled_tasks');
}
@@ -46,9 +46,19 @@ class TaskFailed extends Notification implements ShouldQueue
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
- return "Coolify: Scheduled task ({$this->task->name}, [link]({$this->url})) failed with output: {$this->output}";
+ $message = new DiscordMessage(
+ title: ':cross_mark: Scheduled task failed',
+ description: "Scheduled task ({$this->task->name}) failed.",
+ color: DiscordMessage::errorColor(),
+ );
+
+ if ($this->url) {
+ $message->addField('Scheduled task', '[Link]('.$this->url.')');
+ }
+
+ return $message;
}
public function toTelegram(): array
diff --git a/app/Notifications/Server/DockerCleanup.php b/app/Notifications/Server/DockerCleanup.php
index 682ed7a1a..7ea1b84c2 100644
--- a/app/Notifications/Server/DockerCleanup.php
+++ b/app/Notifications/Server/DockerCleanup.php
@@ -5,6 +5,7 @@ namespace App\Notifications\Server;
use App\Models\Server;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\TelegramChannel;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;
@@ -49,11 +50,13 @@ class DockerCleanup extends Notification implements ShouldQueue
// return $mail;
// }
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
- $message = "Coolify: Server '{$this->server->name}' cleanup job done!\n\n{$this->message}";
-
- return $message;
+ return new DiscordMessage(
+ title: ':white_check_mark: Server cleanup job done',
+ description: $this->message,
+ color: DiscordMessage::successColor(),
+ );
}
public function toTelegram(): array
diff --git a/app/Notifications/Server/ForceDisabled.php b/app/Notifications/Server/ForceDisabled.php
index 6377f2f15..a26c803ee 100644
--- a/app/Notifications/Server/ForceDisabled.php
+++ b/app/Notifications/Server/ForceDisabled.php
@@ -6,6 +6,7 @@ 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;
use Illuminate\Notifications\Messages\MailMessage;
@@ -50,9 +51,15 @@ class ForceDisabled extends Notification implements ShouldQueue
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
- $message = "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subscriptions).";
+ $message = new DiscordMessage(
+ title: ':cross_mark: Server disabled',
+ description: "Server ({$this->server->name}) disabled because it is not paid!",
+ color: DiscordMessage::errorColor(),
+ );
+
+ $message->addField('Please update your subscription to enable the server again!', '[Link](https://app.coolify.io/subscriptions)');
return $message;
}
diff --git a/app/Notifications/Server/ForceEnabled.php b/app/Notifications/Server/ForceEnabled.php
index 83594d643..65b65a10c 100644
--- a/app/Notifications/Server/ForceEnabled.php
+++ b/app/Notifications/Server/ForceEnabled.php
@@ -6,6 +6,7 @@ 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;
use Illuminate\Notifications\Messages\MailMessage;
@@ -50,11 +51,13 @@ class ForceEnabled extends Notification implements ShouldQueue
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
- $message = "Coolify: Server ({$this->server->name}) enabled again!";
-
- return $message;
+ return new DiscordMessage(
+ title: ':white_check_mark: Server enabled',
+ description: "Server '{$this->server->name}' enabled again!",
+ color: DiscordMessage::successColor(),
+ );
}
public function toTelegram(): array
diff --git a/app/Notifications/Server/HighDiskUsage.php b/app/Notifications/Server/HighDiskUsage.php
index 34cb22091..e373abc03 100644
--- a/app/Notifications/Server/HighDiskUsage.php
+++ b/app/Notifications/Server/HighDiskUsage.php
@@ -3,9 +3,7 @@
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;
use Illuminate\Notifications\Messages\MailMessage;
@@ -17,26 +15,11 @@ class HighDiskUsage extends Notification implements ShouldQueue
public $tries = 1;
- public function __construct(public Server $server, public int $disk_usage, public int $docker_cleanup_threshold) {}
+ public function __construct(public Server $server, public int $disk_usage, public int $server_disk_usage_notification_threshold) {}
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;
+ return setNotificationChannels($notifiable, 'server_disk_usage');
}
public function toMail(): MailMessage
@@ -46,15 +29,25 @@ class HighDiskUsage extends Notification implements ShouldQueue
$mail->view('emails.high-disk-usage', [
'name' => $this->server->name,
'disk_usage' => $this->disk_usage,
- 'threshold' => $this->docker_cleanup_threshold,
+ 'threshold' => $this->server_disk_usage_notification_threshold,
]);
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
- $message = "Coolify: Server '{$this->server->name}' high disk usage detected!\nDisk usage: {$this->disk_usage}%. Threshold: {$this->docker_cleanup_threshold}%.\nPlease cleanup your disk to prevent data-loss.\nHere are some tips: https://coolify.io/docs/knowledge-base/server/automated-cleanup.";
+ $message = new DiscordMessage(
+ title: ':cross_mark: High disk usage detected',
+ description: "Server '{$this->server->name}' high disk usage detected!",
+ color: DiscordMessage::errorColor(),
+ isCritical: true,
+ );
+
+ $message->addField('Disk usage', "{$this->disk_usage}%", true);
+ $message->addField('Threshold', "{$this->server_disk_usage_notification_threshold}%", true);
+ $message->addField('What to do?', '[Link](https://coolify.io/docs/knowledge-base/server/automated-cleanup)', true);
+ $message->addField('Change Settings', '[Threshold]('.base_url().'/server/'.$this->server->uuid.'#advanced) | [Notification]('.base_url().'/notifications/discord)');
return $message;
}
@@ -62,7 +55,7 @@ class HighDiskUsage extends Notification implements ShouldQueue
public function toTelegram(): array
{
return [
- 'message' => "Coolify: Server '{$this->server->name}' high disk usage detected!\nDisk usage: {$this->disk_usage}%. Threshold: {$this->docker_cleanup_threshold}%.\nPlease cleanup your disk to prevent data-loss.\nHere are some tips: https://coolify.io/docs/knowledge-base/server/automated-cleanup.",
+ 'message' => "Coolify: Server '{$this->server->name}' high disk usage detected!\nDisk usage: {$this->disk_usage}%. Threshold: {$this->server_disk_usage_notification_threshold}%.\nPlease cleanup your disk to prevent data-loss.\nHere are some tips: https://coolify.io/docs/knowledge-base/server/automated-cleanup.",
];
}
}
diff --git a/app/Notifications/Server/Revived.php b/app/Notifications/Server/Reachable.php
similarity index 64%
rename from app/Notifications/Server/Revived.php
rename to app/Notifications/Server/Reachable.php
index 3f2b3b696..9b54501d9 100644
--- a/app/Notifications/Server/Revived.php
+++ b/app/Notifications/Server/Reachable.php
@@ -2,35 +2,37 @@
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;
use App\Notifications\Channels\TelegramChannel;
+use App\Notifications\Dto\DiscordMessage;
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');
@@ -45,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
@@ -72,11 +62,13 @@ class Revived extends Notification implements ShouldQueue
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
- $message = "Coolify: Server '{$this->server->name}' revived. All automations & integrations are turned on again!";
-
- return $message;
+ return new DiscordMessage(
+ title: ":white_check_mark: Server '{$this->server->name}' revived",
+ description: 'All automations & integrations are turned on again!',
+ color: DiscordMessage::successColor(),
+ );
}
public function toTelegram(): array
diff --git a/app/Notifications/Server/Unreachable.php b/app/Notifications/Server/Unreachable.php
index 2fb83559a..5bc568e82 100644
--- a/app/Notifications/Server/Unreachable.php
+++ b/app/Notifications/Server/Unreachable.php
@@ -6,11 +6,11 @@ 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;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
-use Illuminate\Support\Facades\RateLimiter;
class Unreachable extends Notification implements ShouldQueue
{
@@ -18,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');
@@ -36,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.");
@@ -63,14 +62,20 @@ class Unreachable extends Notification implements ShouldQueue
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): ?DiscordMessage
{
- $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.";
+ $message = new DiscordMessage(
+ title: ':cross_mark: Server unreachable',
+ description: "Your server '{$this->server->name}' is unreachable.",
+ color: DiscordMessage::errorColor(),
+ );
+
+ $message->addField('IMPORTANT', 'We automatically try to revive your server and turn on all automations & integrations.');
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.",
diff --git a/app/Notifications/Test.php b/app/Notifications/Test.php
index 3b46a9a24..a43b1e153 100644
--- a/app/Notifications/Test.php
+++ b/app/Notifications/Test.php
@@ -2,10 +2,12 @@
namespace App\Notifications;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
+use Illuminate\Queue\Middleware\RateLimited;
class Test extends Notification implements ShouldQueue
{
@@ -20,6 +22,14 @@ class Test extends Notification implements ShouldQueue
return setNotificationChannels($notifiable, 'test');
}
+ public function middleware(object $notifiable, string $channel)
+ {
+ return match ($channel) {
+ \App\Notifications\Channels\EmailChannel::class => [new RateLimited('email')],
+ default => [],
+ };
+ }
+
public function toMail(): MailMessage
{
$mail = new MailMessage;
@@ -29,11 +39,15 @@ class Test extends Notification implements ShouldQueue
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
- $message = 'Coolify: This is a test Discord notification from Coolify.';
- $message .= "\n\n";
- $message .= '[Go to your dashboard]('.base_url().')';
+ $message = new DiscordMessage(
+ title: ':white_check_mark: Test Success',
+ description: 'This is a test Discord notification from Coolify. :cross_mark: :warning: :information_source:',
+ color: DiscordMessage::successColor(),
+ );
+
+ $message->addField(name: 'Dashboard', value: '[Link]('.base_url().')', inline: true);
return $message;
}
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
index 8b4c2eef2..015434bd2 100644
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -5,16 +5,30 @@ namespace App\Providers;
use App\Models\PersonalAccessToken;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\ServiceProvider;
+use Illuminate\Validation\Rules\Password;
use Laravel\Sanctum\Sanctum;
class AppServiceProvider extends ServiceProvider
{
- public function register(): void {}
+ public function register(): void
+ {
+ if ($this->app->environment('local')) {
+ $this->app->register(\Laravel\Telescope\TelescopeServiceProvider::class);
+ }
+ }
public function boot(): void
{
Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class);
+ Password::defaults(function () {
+ $rule = Password::min(8);
+
+ return $this->app->isProduction()
+ ? $rule->mixedCase()->letters()->numbers()->symbols()
+ : $rule;
+ });
+
Http::macro('github', function (string $api_url, ?string $github_access_token = null) {
if ($github_access_token) {
return Http::withHeaders([
diff --git a/app/Providers/DuskServiceProvider.php b/app/Providers/DuskServiceProvider.php
new file mode 100644
index 000000000..07e0e8709
--- /dev/null
+++ b/app/Providers/DuskServiceProvider.php
@@ -0,0 +1,21 @@
+visit('/login')
+ ->type('email', 'test@example.com')
+ ->type('password', 'password')
+ ->press('Login');
+ });
+ }
+}
diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php
index b916b6234..e8784bab3 100644
--- a/app/Providers/FortifyServiceProvider.php
+++ b/app/Providers/FortifyServiceProvider.php
@@ -75,7 +75,8 @@ class FortifyServiceProvider extends ServiceProvider
});
Fortify::authenticateUsing(function (Request $request) {
- $user = User::where('email', $request->email)->with('teams')->first();
+ $email = strtolower($request->email);
+ $user = User::where('email', $email)->with('teams')->first();
if (
$user &&
Hash::check($request->password, $user->password)
diff --git a/app/View/Components/Forms/Input.php b/app/View/Components/Forms/Input.php
index fbd7b0b15..7283ef20f 100644
--- a/app/View/Components/Forms/Input.php
+++ b/app/View/Components/Forms/Input.php
@@ -23,6 +23,8 @@ class Input extends Component
public bool $isMultiline = false,
public string $defaultClass = 'input',
public string $autocomplete = 'off',
+ public ?int $minlength = null,
+ public ?int $maxlength = null,
) {}
public function render(): View|Closure|string
diff --git a/app/View/Components/Forms/Textarea.php b/app/View/Components/Forms/Textarea.php
index 3f887877c..6081c2a8a 100644
--- a/app/View/Components/Forms/Textarea.php
+++ b/app/View/Components/Forms/Textarea.php
@@ -30,7 +30,9 @@ class Textarea extends Component
public bool $realtimeValidation = false,
public bool $allowToPeak = true,
public string $defaultClass = 'input scrollbar font-mono',
- public string $defaultClassInput = 'input'
+ public string $defaultClassInput = 'input',
+ public ?int $minlength = null,
+ public ?int $maxlength = null,
) {
//
}
diff --git a/app/View/Components/Server/Sidebar.php b/app/View/Components/Server/Sidebar.php
deleted file mode 100644
index f968b6d0c..000000000
--- a/app/View/Components/Server/Sidebar.php
+++ /dev/null
@@ -1,27 +0,0 @@
-map(function ($d) {
+ return $data->map(function ($d) {
$d = collect($d)->sortKeys();
$created_at = data_get($d, 'created_at');
$updated_at = data_get($d, 'updated_at');
if ($created_at) {
unset($d['created_at']);
$d['created_at'] = $created_at;
-
}
if ($updated_at) {
unset($d['updated_at']);
@@ -50,8 +49,6 @@ function serializeApiResponse($data)
return $d;
});
-
- return $data;
} else {
$d = collect($data)->sortKeys();
$created_at = data_get($d, 'created_at');
@@ -59,7 +56,6 @@ function serializeApiResponse($data)
if ($created_at) {
unset($d['created_at']);
$d['created_at'] = $created_at;
-
}
if ($updated_at) {
unset($d['updated_at']);
diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php
index b3e8011b9..eb331f8c2 100644
--- a/bootstrap/helpers/applications.php
+++ b/bootstrap/helpers/applications.php
@@ -91,7 +91,7 @@ function next_queuable(string $server_id, string $application_id): bool
$server = Server::find($server_id);
$concurrent_builds = $server->settings->concurrent_builds;
- ray("serverId:{$server->id}", "concurrentBuilds:{$concurrent_builds}", "deployments:{$deployments->count()}", "sameApplicationDeployments:{$same_application_deployments->count()}")->green();
+ // ray("serverId:{$server->id}", "concurrentBuilds:{$concurrent_builds}", "deployments:{$deployments->count()}", "sameApplicationDeployments:{$same_application_deployments->count()}")->green();
if ($deployments->count() > $concurrent_builds) {
return false;
diff --git a/bootstrap/helpers/databases.php b/bootstrap/helpers/databases.php
index 950eb67b6..e12910f82 100644
--- a/bootstrap/helpers/databases.php
+++ b/bootstrap/helpers/databases.php
@@ -1,5 +1,6 @@
name = generate_database_name('redis');
- $database->redis_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
+ $redis_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
$database->environment_id = $environment_id;
$database->destination_id = $destination->id;
$database->destination_type = $destination->getMorphClass();
@@ -57,6 +58,20 @@ function create_standalone_redis($environment_id, $destination_uuid, ?array $oth
}
$database->save();
+ EnvironmentVariable::create([
+ 'key' => 'REDIS_PASSWORD',
+ 'value' => $redis_password,
+ 'standalone_redis_id' => $database->id,
+ 'is_shared' => false,
+ ]);
+
+ EnvironmentVariable::create([
+ 'key' => 'REDIS_USERNAME',
+ 'value' => 'default',
+ 'standalone_redis_id' => $database->id,
+ 'is_shared' => false,
+ ]);
+
return $database;
}
diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php
index 397bce029..2e583b94d 100644
--- a/bootstrap/helpers/docker.php
+++ b/bootstrap/helpers/docker.php
@@ -32,9 +32,8 @@ function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pul
return null;
});
- $containers = $containers->filter();
- return $containers;
+ return $containers->filter();
}
return $containers;
@@ -46,9 +45,8 @@ function getCurrentServiceContainerStatus(Server $server, int $id): Collection
if (! $server->isSwarm()) {
$containers = instant_remote_process(["docker ps -a --filter='label=coolify.serviceId={$id}' --format '{{json .}}' "], $server);
$containers = format_docker_command_output_to_json($containers);
- $containers = $containers->filter();
- return $containers;
+ return $containers->filter();
}
return $containers;
@@ -67,7 +65,7 @@ function format_docker_command_output_to_json($rawOutput): Collection
return $outputLines
->reject(fn ($line) => empty($line))
->map(fn ($outputLine) => json_decode($outputLine, true, flags: JSON_THROW_ON_ERROR));
- } catch (\Throwable $e) {
+ } catch (\Throwable) {
return collect([]);
}
}
@@ -104,7 +102,7 @@ function format_docker_envs_to_json($rawOutput)
return [$env[0] => $env[1]];
});
- } catch (\Throwable $e) {
+ } catch (\Throwable) {
return collect([]);
}
}
@@ -207,12 +205,12 @@ function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'applica
}
function generateServiceSpecificFqdns(ServiceApplication|Application $resource)
{
- if ($resource->getMorphClass() === 'App\Models\ServiceApplication') {
+ if ($resource->getMorphClass() === \App\Models\ServiceApplication::class) {
$uuid = data_get($resource, 'uuid');
$server = data_get($resource, 'service.server');
$environment_variables = data_get($resource, 'service.environment_variables');
$type = $resource->serviceType();
- } elseif ($resource->getMorphClass() === 'App\Models\Application') {
+ } elseif ($resource->getMorphClass() === \App\Models\Application::class) {
$uuid = data_get($resource, 'uuid');
$server = data_get($resource, 'destination.server');
$environment_variables = data_get($resource, 'environment_variables');
@@ -279,7 +277,6 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains,
$labels->push("caddy_ingress_network={$network}");
}
foreach ($domains as $loop => $domain) {
- $loop = $loop;
$url = Url::fromString($domain);
$host = $url->getHost();
$path = $url->getPath();
@@ -335,10 +332,11 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
if (preg_match('/coolify\.traefik\.middlewares=(.*)/', $item, $matches)) {
return explode(',', $matches[1]);
}
+
return null;
})->flatten()
- ->filter()
- ->unique();
+ ->filter()
+ ->unique();
}
foreach ($domains as $loop => $domain) {
try {
@@ -388,7 +386,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
if ($path !== '/') {
// Middleware handling
$middlewares = collect([]);
- if ($is_stripprefix_enabled && !str($image)->contains('ghost')) {
+ if ($is_stripprefix_enabled && ! str($image)->contains('ghost')) {
$labels->push("traefik.http.middlewares.{$https_label}-stripprefix.stripprefix.prefixes={$path}");
$middlewares->push("{$https_label}-stripprefix");
}
@@ -402,7 +400,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels = $labels->merge($redirect_to_non_www);
$middlewares->push($to_non_www_name);
}
- if ($redirect_direction === 'www' && !str($host)->startsWith('www.')) {
+ if ($redirect_direction === 'www' && ! str($host)->startsWith('www.')) {
$labels = $labels->merge($redirect_to_www);
$middlewares->push($to_www_name);
}
@@ -417,7 +415,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$middlewares = collect([]);
if ($is_gzip_enabled) {
$middlewares->push('gzip');
- }
+ }
if (str($image)->contains('ghost')) {
$middlewares->push('redir-ghost');
}
@@ -510,7 +508,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
}
}
}
- } catch (\Throwable $e) {
+ } catch (\Throwable) {
continue;
}
}
@@ -581,7 +579,6 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
redirect_direction: $application->redirect
));
}
-
}
} else {
if (data_get($preview, 'fqdn')) {
@@ -633,7 +630,6 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
is_stripprefix_enabled: $application->isStripprefixEnabled()
));
}
-
}
return $labels->all();
@@ -658,7 +654,7 @@ function isDatabaseImage(?string $image = null)
return false;
}
-function convert_docker_run_to_compose(?string $custom_docker_run_options = null)
+function convertDockerRunToCompose(?string $custom_docker_run_options = null)
{
$options = [];
$compose_options = collect([]);
@@ -683,9 +679,17 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null
'--privileged' => 'privileged',
'--ip' => 'ip',
'--shm-size' => 'shm_size',
+ '--gpus' => 'gpus',
]);
foreach ($matches as $match) {
$option = $match[1];
+ if ($option === '--gpus') {
+ $regexForParsingDeviceIds = '/device=([0-9A-Za-z-,]+)/';
+ preg_match($regexForParsingDeviceIds, $custom_docker_run_options, $device_matches);
+ $value = $device_matches[1] ?? 'all';
+ $options[$option][] = $value;
+ $options[$option] = array_unique($options[$option]);
+ }
if (isset($match[2]) && $match[2] !== '') {
$value = $match[2];
$options[$option][] = $value;
@@ -698,7 +702,6 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null
$options = collect($options);
// Easily get mappings from https://github.com/composerize/composerize/blob/master/packages/composerize/src/mappings.js
foreach ($options as $option => $value) {
- // ray($option,$value);
if (! data_get($mapping, $option)) {
continue;
}
@@ -727,6 +730,28 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null
if (! is_null($value) && is_array($value) && count($value) > 0) {
$compose_options->put($mapping[$option], $value[0]);
}
+ } elseif ($option === '--gpus') {
+ $payload = [
+ 'driver' => 'nvidia',
+ 'capabilities' => ['gpu'],
+ ];
+ if (! is_null($value) && is_array($value) && count($value) > 0) {
+ if (str($value[0]) != 'all') {
+ if (str($value[0])->contains(',')) {
+ $payload['device_ids'] = str($value[0])->explode(',')->toArray();
+ } else {
+ $payload['device_ids'] = [$value[0]];
+ }
+ }
+ }
+ ray($payload);
+ $compose_options->put('deploy', [
+ 'resources' => [
+ 'reservations' => [
+ 'devices' => [$payload],
+ ],
+ ],
+ ]);
} else {
if ($list_options->contains($option)) {
if ($compose_options->has($mapping[$option])) {
@@ -748,7 +773,7 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null
return $compose_options->toArray();
}
-function generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $network)
+function generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $network)
{
$ipv4 = data_get($docker_run_options, 'ip.0');
$ipv6 = data_get($docker_run_options, 'ip6.0');
diff --git a/bootstrap/helpers/github.php b/bootstrap/helpers/github.php
index 97deb0b1c..529ac82b1 100644
--- a/bootstrap/helpers/github.php
+++ b/bootstrap/helpers/github.php
@@ -3,6 +3,7 @@
use App\Models\GithubApp;
use App\Models\GitlabApp;
use Carbon\Carbon;
+use Carbon\CarbonImmutable;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Lcobucci\JWT\Encoding\ChainedFormatter;
@@ -16,7 +17,7 @@ function generate_github_installation_token(GithubApp $source)
$signingKey = InMemory::plainText($source->privateKey->private_key);
$algorithm = new Sha256;
$tokenBuilder = (new Builder(new JoseEncoder, ChainedFormatter::default()));
- $now = new DateTimeImmutable;
+ $now = CarbonImmutable::now();
$now = $now->setTime($now->format('H'), $now->format('i'));
$issuedToken = $tokenBuilder
->issuedBy($source->app_id)
@@ -40,16 +41,15 @@ function generate_github_jwt_token(GithubApp $source)
$signingKey = InMemory::plainText($source->privateKey->private_key);
$algorithm = new Sha256;
$tokenBuilder = (new Builder(new JoseEncoder, ChainedFormatter::default()));
- $now = new DateTimeImmutable;
+ $now = CarbonImmutable::now();
$now = $now->setTime($now->format('H'), $now->format('i'));
- $issuedToken = $tokenBuilder
+
+ return $tokenBuilder
->issuedBy($source->app_id)
->issuedAt($now->modify('-1 minute'))
->expiresAt($now->modify('+10 minutes'))
->getToken($algorithm, $signingKey)
->toString();
-
- return $issuedToken;
}
function githubApi(GithubApp|GitlabApp|null $source, string $endpoint, string $method = 'get', ?array $data = null, bool $throwError = true)
@@ -57,7 +57,7 @@ function githubApi(GithubApp|GitlabApp|null $source, string $endpoint, string $m
if (is_null($source)) {
throw new \Exception('Not implemented yet.');
}
- if ($source->getMorphClass() == 'App\Models\GithubApp') {
+ if ($source->getMorphClass() === \App\Models\GithubApp::class) {
if ($source->is_public) {
$response = Http::github($source->api_url)->$method($endpoint);
} else {
diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php
index 309ccee4a..a8ef0fe5a 100644
--- a/bootstrap/helpers/proxy.php
+++ b/bootstrap/helpers/proxy.php
@@ -16,12 +16,10 @@ function collectProxyDockerNetworksByServer(Server $server)
return collect();
}
$networks = instant_remote_process(['docker inspect --format="{{json .NetworkSettings.Networks }}" coolify-proxy'], $server, false);
- $networks = collect($networks)->map(function ($network) {
+
+ return collect($networks)->map(function ($network) {
return collect(json_decode($network))->keys();
})->flatten()->unique();
-
- return $networks;
-
}
function collectDockerNetworksByServer(Server $server)
{
@@ -241,9 +239,11 @@ function generate_default_proxy_configuration(Server $server)
'ports' => [
'80:80',
'443:443',
+ '443:443/udp',
],
'labels' => [
'coolify.managed=true',
+ 'coolify.proxy=true',
],
'volumes' => [
'/var/run/docker.sock:/var/run/docker.sock:ro',
diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php
index 67b60d6b7..c7dd2cb83 100644
--- a/bootstrap/helpers/remoteProcess.php
+++ b/bootstrap/helpers/remoteProcess.php
@@ -124,7 +124,7 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d
associative: true,
flags: JSON_THROW_ON_ERROR
);
- } catch (\JsonException $exception) {
+ } catch (\JsonException) {
return collect([]);
}
$seenCommands = collect();
@@ -204,7 +204,7 @@ function checkRequiredCommands(Server $server)
}
try {
instant_remote_process(["docker run --rm --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c 'apt update && apt install -y {$command}'"], $server);
- } catch (\Throwable $e) {
+ } catch (\Throwable) {
break;
}
$commandFound = instant_remote_process(["docker run --rm --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c 'command -v {$command}'"], $server, false);
diff --git a/bootstrap/helpers/services.php b/bootstrap/helpers/services.php
index eba88d000..fd2e1231f 100644
--- a/bootstrap/helpers/services.php
+++ b/bootstrap/helpers/services.php
@@ -24,7 +24,7 @@ function replaceVariables(string $variable): Stringable
function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Application $oneService, bool $isInit = false)
{
try {
- if ($oneService->getMorphClass() === 'App\Models\Application') {
+ if ($oneService->getMorphClass() === \App\Models\Application::class) {
$workdir = $oneService->workdir();
$server = $oneService->destination->server;
} else {
@@ -51,7 +51,7 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Appli
// Exists and is a directory
$isDir = instant_remote_process(["test -d $fileLocation && echo OK || echo NOK"], $server);
- if ($isFile == 'OK') {
+ if ($isFile === 'OK') {
// If its a file & exists
$filesystemContent = instant_remote_process(["cat $fileLocation"], $server);
if ($fileVolume->is_based_on_git) {
@@ -59,12 +59,12 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Appli
}
$fileVolume->is_directory = false;
$fileVolume->save();
- } elseif ($isDir == 'OK') {
+ } elseif ($isDir === 'OK') {
// If its a directory & exists
$fileVolume->content = null;
$fileVolume->is_directory = true;
$fileVolume->save();
- } elseif ($isFile == 'NOK' && $isDir == 'NOK' && ! $fileVolume->is_directory && $isInit && $content) {
+ } elseif ($isFile === 'NOK' && $isDir === 'NOK' && ! $fileVolume->is_directory && $isInit && $content) {
// Does not exists (no dir or file), not flagged as directory, is init, has content
$fileVolume->content = $content;
$fileVolume->is_directory = false;
@@ -75,13 +75,13 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Appli
"mkdir -p $dir",
"echo '$content' | base64 -d | tee $fileLocation",
], $server);
- } elseif ($isFile == 'NOK' && $isDir == 'NOK' && $fileVolume->is_directory && $isInit) {
+ } elseif ($isFile === 'NOK' && $isDir === 'NOK' && $fileVolume->is_directory && $isInit) {
// Does not exists (no dir or file), flagged as directory, is init
$fileVolume->content = null;
$fileVolume->is_directory = true;
$fileVolume->save();
instant_remote_process(["mkdir -p $fileLocation"], $server);
- } elseif ($isFile == 'NOK' && $isDir == 'NOK' && ! $fileVolume->is_directory && $isInit && is_null($content)) {
+ } elseif ($isFile === 'NOK' && $isDir === 'NOK' && ! $fileVolume->is_directory && $isInit && is_null($content)) {
// Does not exists (no dir or file), not flagged as directory, is init, has no content => create directory
$fileVolume->content = null;
$fileVolume->is_directory = true;
@@ -245,8 +245,5 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource)
}
function serviceKeys()
{
- $services = get_service_templates();
- $serviceKeys = $services->keys();
-
- return $serviceKeys;
+ return get_service_templates()->keys();
}
diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php
index cfdea81fb..f6875cc81 100644
--- a/bootstrap/helpers/shared.php
+++ b/bootstrap/helpers/shared.php
@@ -28,17 +28,20 @@ use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\TelegramChannel;
use App\Notifications\Internal\GeneralNotification;
+use Carbon\CarbonImmutable;
use DanHarrin\LivewireRateLimiting\Exceptions\TooManyRequestsException;
use Illuminate\Database\UniqueConstraintViolationException;
use Illuminate\Mail\Message;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Process\Pool;
use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Process;
+use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Validator;
@@ -98,12 +101,12 @@ function isInstanceAdmin()
function currentTeam()
{
- return auth()?->user()?->currentTeam() ?? null;
+ return Auth::user()?->currentTeam() ?? null;
}
function showBoarding(): bool
{
- if (auth()->user()?->isMember()) {
+ if (Auth::user()?->isMember()) {
return false;
}
@@ -112,21 +115,20 @@ function showBoarding(): bool
function refreshSession(?Team $team = null): void
{
if (! $team) {
- if (auth()->user()?->currentTeam()) {
- $team = Team::find(auth()->user()->currentTeam()->id);
+ if (Auth::user()->currentTeam()) {
+ $team = Team::find(Auth::user()->currentTeam()->id);
} else {
- $team = User::find(auth()->user()->id)->teams->first();
+ $team = User::find(Auth::id())->teams->first();
}
}
- Cache::forget('team:'.auth()->user()->id);
- Cache::remember('team:'.auth()->user()->id, 3600, function () use ($team) {
+ Cache::forget('team:'.Auth::id());
+ Cache::remember('team:'.Auth::id(), 3600, function () use ($team) {
return $team;
});
session(['currentTeam' => $team]);
}
function handleError(?Throwable $error = null, ?Livewire\Component $livewire = null, ?string $customErrorMessage = null)
{
- ray($error);
if ($error instanceof TooManyRequestsException) {
if (isset($livewire)) {
return $livewire->dispatch('error', "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds.");
@@ -142,6 +144,10 @@ function handleError(?Throwable $error = null, ?Livewire\Component $livewire = n
return 'Duplicate entry found. Please use a different name.';
}
+ if ($error instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) {
+ abort(404);
+ }
+
if ($error instanceof Throwable) {
$message = $error->getMessage();
} else {
@@ -164,14 +170,11 @@ function get_route_parameters(): array
function get_latest_sentinel_version(): string
{
try {
- $response = Http::get('https://cdn.coollabs.io/sentinel/versions.json');
+ $response = Http::get('https://cdn.coollabs.io/coolify/versions.json');
$versions = $response->json();
- return data_get($versions, 'sentinel.version');
- } catch (\Throwable $e) {
- //throw $e;
- ray($e->getMessage());
-
+ return data_get($versions, 'coolify.sentinel.version');
+ } catch (\Throwable) {
return '0.0.0';
}
}
@@ -300,7 +303,7 @@ function getFqdnWithoutPort(string $fqdn)
$path = $url->getPath();
return "$scheme://$host$path";
- } catch (\Throwable $e) {
+ } catch (\Throwable) {
return $fqdn;
}
}
@@ -368,6 +371,9 @@ function translate_cron_expression($expression_to_validate): string
}
function validate_cron_expression($expression_to_validate): bool
{
+ if (empty($expression_to_validate)) {
+ return false;
+ }
$isValid = false;
$expression = new CronExpression($expression_to_validate);
$isValid = $expression->isValid();
@@ -496,9 +502,8 @@ function generateFqdn(Server $server, string $random, bool $forceHttps = false):
if ($forceHttps) {
$scheme = 'https';
}
- $finalFqdn = "$scheme://{$random}.$host$path";
- return $finalFqdn;
+ return "$scheme://{$random}.$host$path";
}
function sslip(Server $server)
{
@@ -536,7 +541,7 @@ function get_service_templates(bool $force = false): Collection
$services = $response->json();
return collect($services);
- } catch (\Throwable $e) {
+ } catch (\Throwable) {
$services = File::get(base_path('templates/service-templates.json'));
return collect(json_decode($services))->sortKeys();
@@ -643,14 +648,13 @@ function queryResourcesByUuid(string $uuid)
return $resource;
}
-function generatTagDeployWebhook($tag_name)
+function generateTagDeployWebhook($tag_name)
{
$baseUrl = base_url();
$api = Url::fromString($baseUrl).'/api/v1';
$endpoint = "/deploy?tag=$tag_name";
- $url = $api.$endpoint;
- return $url;
+ return $api.$endpoint;
}
function generateDeployWebhook($resource)
{
@@ -658,20 +662,18 @@ function generateDeployWebhook($resource)
$api = Url::fromString($baseUrl).'/api/v1';
$endpoint = '/deploy';
$uuid = data_get($resource, 'uuid');
- $url = $api.$endpoint."?uuid=$uuid&force=false";
- return $url;
+ return $api.$endpoint."?uuid=$uuid&force=false";
}
function generateGitManualWebhook($resource, $type)
{
if ($resource->source_id !== 0 && ! is_null($resource->source_id)) {
return null;
}
- if ($resource->getMorphClass() === 'App\Models\Application') {
+ if ($resource->getMorphClass() === \App\Models\Application::class) {
$baseUrl = base_url();
- $api = Url::fromString($baseUrl)."/webhooks/source/$type/events/manual";
- return $api;
+ return Url::fromString($baseUrl)."/webhooks/source/$type/events/manual";
}
return null;
@@ -683,7 +685,7 @@ function removeAnsiColors($text)
function getTopLevelNetworks(Service|Application $resource)
{
- if ($resource->getMorphClass() === 'App\Models\Service') {
+ if ($resource->getMorphClass() === \App\Models\Service::class) {
if ($resource->docker_compose_raw) {
try {
$yaml = Yaml::parse($resource->docker_compose_raw);
@@ -738,7 +740,7 @@ function getTopLevelNetworks(Service|Application $resource)
return $topLevelNetworks->keys();
}
- } elseif ($resource->getMorphClass() === 'App\Models\Application') {
+ } elseif ($resource->getMorphClass() === \App\Models\Application::class) {
try {
$yaml = Yaml::parse($resource->docker_compose_raw);
} catch (\Exception $e) {
@@ -945,7 +947,7 @@ function generateEnvValue(string $command, Service|Application|null $service = n
$key = InMemory::plainText($signingKey);
$algorithm = new Sha256;
$tokenBuilder = (new Builder(new JoseEncoder, ChainedFormatter::default()));
- $now = new DateTimeImmutable;
+ $now = CarbonImmutable::now();
$now = $now->setTime($now->format('H'), $now->format('i'));
$token = $tokenBuilder
->issuedBy('supabase')
@@ -965,7 +967,7 @@ function generateEnvValue(string $command, Service|Application|null $service = n
$key = InMemory::plainText($signingKey);
$algorithm = new Sha256;
$tokenBuilder = (new Builder(new JoseEncoder, ChainedFormatter::default()));
- $now = new DateTimeImmutable;
+ $now = CarbonImmutable::now();
$now = $now->setTime($now->format('H'), $now->format('i'));
$token = $tokenBuilder
->issuedBy('supabase')
@@ -1048,7 +1050,7 @@ function validate_dns_entry(string $fqdn, Server $server)
}
}
}
- } catch (\Exception $e) {
+ } catch (\Exception) {
}
}
ray("Found match: $found_matching_ip");
@@ -1145,7 +1147,7 @@ function checkIfDomainIsAlreadyUsed(Collection|array $domains, ?string $teamId =
function check_domain_usage(ServiceApplication|Application|null $resource = null, ?string $domain = null)
{
if ($resource) {
- if ($resource->getMorphClass() === 'App\Models\Application' && $resource->build_pack === 'dockercompose') {
+ if ($resource->getMorphClass() === \App\Models\Application::class && $resource->build_pack === 'dockercompose') {
$domains = data_get(json_decode($resource->docker_compose_domains, true), '*.domain');
$domains = collect($domains);
} else {
@@ -1338,13 +1340,6 @@ function isAnyDeploymentInprogress()
exit(0);
}
-function generateSentinelToken()
-{
- $token = Str::random(64);
-
- return $token;
-}
-
function isBase64Encoded($strValue)
{
return base64_encode(base64_decode($strValue, true)) === $strValue;
@@ -1416,7 +1411,7 @@ function parseServiceVolumes($serviceVolumes, $resource, $topLevelVolumes, $pull
if ($source->value() === '/tmp' || $source->value() === '/tmp/') {
return $volume;
}
- if (get_class($resource) === "App\Models\Application") {
+ if (get_class($resource) === \App\Models\Application::class) {
$dir = base_configuration_dir().'/applications/'.$resource->uuid;
} else {
$dir = base_configuration_dir().'/services/'.$resource->service->uuid;
@@ -1456,7 +1451,7 @@ function parseServiceVolumes($serviceVolumes, $resource, $topLevelVolumes, $pull
}
}
$slugWithoutUuid = Str::slug($source, '-');
- if (get_class($resource) === "App\Models\Application") {
+ if (get_class($resource) === \App\Models\Application::class) {
$name = "{$resource->uuid}_{$slugWithoutUuid}";
} else {
$name = "{$resource->service->uuid}_{$slugWithoutUuid}";
@@ -1499,7 +1494,7 @@ function parseServiceVolumes($serviceVolumes, $resource, $topLevelVolumes, $pull
function parseDockerComposeFile(Service|Application $resource, bool $isNew = false, int $pull_request_id = 0, ?int $preview_id = null)
{
- if ($resource->getMorphClass() === 'App\Models\Service') {
+ if ($resource->getMorphClass() === \App\Models\Service::class) {
if ($resource->docker_compose_raw) {
try {
$yaml = Yaml::parse($resource->docker_compose_raw);
@@ -2213,10 +2208,10 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
} else {
return collect([]);
}
- } elseif ($resource->getMorphClass() === 'App\Models\Application') {
+ } elseif ($resource->getMorphClass() === \App\Models\Application::class) {
try {
$yaml = Yaml::parse($resource->docker_compose_raw);
- } catch (\Exception $e) {
+ } catch (\Exception) {
return;
}
$server = $resource->destination->server;
@@ -2962,7 +2957,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
try {
$yaml = Yaml::parse($compose);
- } catch (\Exception $e) {
+ } catch (\Exception) {
return collect([]);
}
@@ -3099,7 +3094,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
}
}
- if ($value && get_class($value) === 'Illuminate\Support\Stringable' && $value->startsWith('/')) {
+ if ($value && get_class($value) === \Illuminate\Support\Stringable::class && $value->startsWith('/')) {
$path = $value->value();
if ($path !== '/') {
$fqdn = "$fqdn$path";
@@ -3190,7 +3185,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
'is_build_time' => false,
'is_preview' => false,
]);
-
} else {
$value = generateEnvValue($command, $resource);
$resource->environment_variables()->where('key', $key->value())->where($nameOfId, $resource->id)->firstOrCreate([
@@ -3569,6 +3563,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
]);
} else {
if ($value->startsWith('$')) {
+ $isRequired = false;
if ($value->contains(':-')) {
$value = replaceVariables($value);
$key = $value->before(':');
@@ -3583,11 +3578,13 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$key = $value->before(':');
$value = $value->after(':?');
+ $isRequired = true;
} elseif ($value->contains('?')) {
$value = replaceVariables($value);
$key = $value->before('?');
$value = $value->after('?');
+ $isRequired = true;
}
if ($originalValue->value() === $value->value()) {
// This means the variable does not have a default value, so it needs to be created in Coolify
@@ -3598,6 +3595,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
], [
'is_build_time' => false,
'is_preview' => false,
+ 'is_required' => $isRequired,
]);
// Add the variable to the environment so it will be shown in the deployable compose file
$environment[$parsedKeyValue->value()] = $resource->environment_variables()->where('key', $parsedKeyValue)->where($nameOfId, $resource->id)->first()->value;
@@ -3611,9 +3609,9 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
'value' => $value,
'is_build_time' => false,
'is_preview' => false,
+ 'is_required' => $isRequired,
]);
}
-
}
}
if ($isApplication) {
@@ -3787,7 +3785,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
service_name: $serviceName,
image: $image,
predefinedPort: $predefinedPort
-
));
}
}
@@ -3978,20 +3975,19 @@ function convertComposeEnvironmentToArray($environment)
}
return $convertedServiceVariables;
-
}
function instanceSettings()
{
return InstanceSettings::get();
}
-function loadConfigFromGit(string $repository, string $branch, string $base_directory, int $server_id, int $team_id) {
-
+function loadConfigFromGit(string $repository, string $branch, string $base_directory, int $server_id, int $team_id)
+{
$server = Server::find($server_id)->where('team_id', $team_id)->first();
- if (!$server) {
+ if (! $server) {
return;
}
- $uuid = new Cuid2();
+ $uuid = new Cuid2;
$cloneCommand = "git clone --no-checkout -b $branch $repository .";
$workdir = rtrim($base_directory, '/');
$fileList = collect([".$workdir/coolify.json"]);
@@ -4008,7 +4004,61 @@ function loadConfigFromGit(string $repository, string $branch, string $base_dire
]);
try {
return instant_remote_process($commands, $server);
- } catch (\Exception $e) {
- // continue
+ } catch (\Exception) {
+ // continue
}
}
+
+function loggy($message = null, array $context = [])
+{
+ if (! isDev()) {
+ return;
+ }
+ if (function_exists('ray') && config('app.debug')) {
+ ray($message, $context);
+ }
+ if (is_null($message)) {
+ return app('log');
+ }
+
+ return app('log')->debug($message, $context);
+}
+function sslipDomainWarning(string $domains)
+{
+ $domains = str($domains)->trim()->explode(',');
+ $showSslipHttpsWarning = false;
+ $domains->each(function ($domain) use (&$showSslipHttpsWarning) {
+ if (str($domain)->contains('https') && str($domain)->contains('sslip')) {
+ $showSslipHttpsWarning = true;
+ }
+ });
+
+ return $showSslipHttpsWarning;
+}
+
+function isEmailRateLimited(string $limiterKey, int $decaySeconds = 3600, ?callable $callbackOnSuccess = null): bool
+{
+ if (isDev()) {
+ $decaySeconds = 120;
+ }
+ $rateLimited = false;
+ $executed = RateLimiter::attempt(
+ $limiterKey,
+ $maxAttempts = 0,
+ function () use (&$rateLimited, &$limiterKey, $callbackOnSuccess) {
+ isDev() && loggy('Rate limit not reached for '.$limiterKey);
+ $rateLimited = false;
+
+ if ($callbackOnSuccess) {
+ $callbackOnSuccess();
+ }
+ },
+ $decaySeconds,
+ );
+ if (! $executed) {
+ isDev() && loggy('Rate limit reached for '.$limiterKey.'. Rate limiter will be disabled for '.$decaySeconds.' seconds.');
+ $rateLimited = true;
+ }
+
+ return $rateLimited;
+}
diff --git a/bootstrap/helpers/socialite.php b/bootstrap/helpers/socialite.php
index a23dc24d3..cad9de7fa 100644
--- a/bootstrap/helpers/socialite.php
+++ b/bootstrap/helpers/socialite.php
@@ -7,7 +7,7 @@ function get_socialite_provider(string $provider)
{
$oauth_setting = OauthSetting::firstWhere('provider', $provider);
- if ($provider == 'azure') {
+ if ($provider === 'azure') {
$azure_config = new \SocialiteProviders\Manager\Config(
$oauth_setting->client_id,
$oauth_setting->client_secret,
diff --git a/bootstrap/helpers/subscriptions.php b/bootstrap/helpers/subscriptions.php
index aadd2dd34..8ddb1331c 100644
--- a/bootstrap/helpers/subscriptions.php
+++ b/bootstrap/helpers/subscriptions.php
@@ -55,12 +55,11 @@ function getStripeCustomerPortalSession(Team $team)
if (! $stripe_customer_id) {
return null;
}
- $session = \Stripe\BillingPortal\Session::create([
+
+ return \Stripe\BillingPortal\Session::create([
'customer' => $stripe_customer_id,
'return_url' => $return_url,
]);
-
- return $session;
}
function allowedPathsForUnsubscribedAccounts()
{
diff --git a/composer.json b/composer.json
index fbd77d0cf..2bae1149c 100644
--- a/composer.json
+++ b/composer.json
@@ -1,25 +1,28 @@
{
- "name": "laravel/laravel",
+ "name": "coollabsio/coolify",
+ "description": "The Coolify project.",
+ "license": "Apache-2.0",
"type": "project",
- "description": "The Laravel Framework.",
"keywords": [
- "framework",
- "laravel"
+ "coolify",
+ "deployment",
+ "docker",
+ "self-hosted",
+ "server"
],
- "license": "MIT",
"require": {
"php": "^8.2",
"danharrin/livewire-rate-limiting": "^1.1",
"doctrine/dbal": "^3.6",
"guzzlehttp/guzzle": "^7.5.0",
- "laravel/fortify": "^v1.16.0",
- "laravel/framework": "^v11",
+ "laravel/fortify": "^1.16.0",
+ "laravel/framework": "^11",
"laravel/horizon": "^5.29.1",
+ "laravel/pail": "^1.1",
"laravel/prompts": "^0.1.6",
- "laravel/sanctum": "^v4.0",
- "laravel/socialite": "^v5.14.0",
- "laravel/telescope": "^5.2",
- "laravel/tinker": "^v2.8.1",
+ "laravel/sanctum": "^4.0",
+ "laravel/socialite": "^5.14.0",
+ "laravel/tinker": "^2.8.1",
"laravel/ui": "^4.2",
"lcobucci/jwt": "^5.0.0",
"league/flysystem-aws-s3-v3": "^3.0",
@@ -28,7 +31,7 @@
"log1x/laravel-webfonts": "^1.0",
"lorisleiva/laravel-actions": "^2.7",
"nubs/random-name-generator": "^2.2",
- "phpseclib/phpseclib": "~3.0",
+ "phpseclib/phpseclib": "^3.0",
"pion/laravel-chunk-upload": "^1.5",
"poliander/cron": "^3.0",
"purplepixie/phpdns": "^2.1",
@@ -49,53 +52,44 @@
},
"require-dev": {
"barryvdh/laravel-debugbar": "^3.13",
- "fakerphp/faker": "^v1.21.0",
- "laravel/dusk": "^v8.0",
+ "fakerphp/faker": "^1.21.0",
+ "laravel/dusk": "^8.0",
"laravel/pint": "^1.16",
+ "laravel/telescope": "^5.2",
"mockery/mockery": "^1.5.1",
- "nunomaduro/collision": "^v8.1",
+ "nunomaduro/collision": "^8.1",
"pestphp/pest": "^2.16",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^10.0.19",
- "serversideup/spin": "^v1.1.0",
+ "serversideup/spin": "^1.1.0",
"spatie/laravel-ignition": "^2.1.0",
"symfony/http-client": "^6.2"
},
+ "minimum-stability": "stable",
+ "prefer-stable": true,
"autoload": {
- "files": [
- "bootstrap/includeHelpers.php"
- ],
"psr-4": {
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
- }
+ },
+ "files": [
+ "bootstrap/includeHelpers.php"
+ ]
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
- "scripts": {
- "post-autoload-dump": [
- "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
- "@php artisan package:discover --ansi"
- ],
- "post-update-cmd": [
- "@php artisan vendor:publish --tag=laravel-assets --ansi --force",
- "Illuminate\\Foundation\\ComposerScripts::postUpdate"
- ],
- "post-install-cmd": [
- "cp -r 'hooks/' '.git/hooks/'",
- "php -r \"copy('hooks/pre-commit', '.git/hooks/pre-commit');\"",
- "php -r \"chmod('.git/hooks/pre-commit', 0777);\""
- ],
- "post-root-package-install": [
- "@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
- ],
- "post-create-project-cmd": [
- "@php artisan key:generate --ansi"
- ]
+ "config": {
+ "allow-plugins": {
+ "pestphp/pest-plugin": true,
+ "php-http/discovery": true
+ },
+ "optimize-autoloader": true,
+ "preferred-install": "dist",
+ "sort-packages": true
},
"extra": {
"laravel": {
@@ -104,15 +98,25 @@
]
}
},
- "config": {
- "optimize-autoloader": true,
- "preferred-install": "dist",
- "sort-packages": true,
- "allow-plugins": {
- "pestphp/pest-plugin": true,
- "php-http/discovery": true
- }
- },
- "minimum-stability": "stable",
- "prefer-stable": true
-}
+ "scripts": {
+ "post-install-cmd": [
+ "cp -r 'hooks/' '.git/hooks/'",
+ "php -r \"copy('hooks/pre-commit', '.git/hooks/pre-commit');\"",
+ "php -r \"chmod('.git/hooks/pre-commit', 0777);\""
+ ],
+ "post-update-cmd": [
+ "@php artisan vendor:publish --tag=laravel-assets --ansi --force",
+ "Illuminate\\Foundation\\ComposerScripts::postUpdate"
+ ],
+ "post-autoload-dump": [
+ "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
+ "@php artisan package:discover --ansi"
+ ],
+ "post-root-package-install": [
+ "@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
+ ],
+ "post-create-project-cmd": [
+ "@php artisan key:generate --ansi"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/composer.lock b/composer.lock
index 0b8da82d0..5eb03b5fc 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "c47adf3684eb727e22503937435c0914",
+ "content-hash": "3f2342fe6b1ba920c8875f8a8fe41962",
"packages": [
{
"name": "amphp/amp",
@@ -3144,6 +3144,83 @@
},
"time": "2024-10-08T18:23:02+00:00"
},
+ {
+ "name": "laravel/pail",
+ "version": "v1.1.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laravel/pail.git",
+ "reference": "b33ad8321416fe86efed7bf398f3306c47b4871b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laravel/pail/zipball/b33ad8321416fe86efed7bf398f3306c47b4871b",
+ "reference": "b33ad8321416fe86efed7bf398f3306c47b4871b",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "illuminate/console": "^10.24|^11.0",
+ "illuminate/contracts": "^10.24|^11.0",
+ "illuminate/log": "^10.24|^11.0",
+ "illuminate/process": "^10.24|^11.0",
+ "illuminate/support": "^10.24|^11.0",
+ "nunomaduro/termwind": "^1.15|^2.0",
+ "php": "^8.2",
+ "symfony/console": "^6.0|^7.0"
+ },
+ "require-dev": {
+ "laravel/pint": "^1.13",
+ "orchestra/testbench": "^8.12|^9.0",
+ "pestphp/pest": "^2.20",
+ "pestphp/pest-plugin-type-coverage": "^2.3",
+ "phpstan/phpstan": "^1.10",
+ "symfony/var-dumper": "^6.3|^7.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.x-dev"
+ },
+ "laravel": {
+ "providers": [
+ "Laravel\\Pail\\PailServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Laravel\\Pail\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Taylor Otwell",
+ "email": "taylor@laravel.com"
+ },
+ {
+ "name": "Nuno Maduro",
+ "email": "enunomaduro@gmail.com"
+ }
+ ],
+ "description": "Easily delve into your Laravel application's log files directly from the command line.",
+ "homepage": "https://github.com/laravel/pail",
+ "keywords": [
+ "laravel",
+ "logs",
+ "php",
+ "tail"
+ ],
+ "support": {
+ "issues": "https://github.com/laravel/pail/issues",
+ "source": "https://github.com/laravel/pail"
+ },
+ "time": "2024-10-15T20:06:24+00:00"
+ },
{
"name": "laravel/prompts",
"version": "v0.1.25",
@@ -3399,75 +3476,6 @@
},
"time": "2024-09-03T09:46:57+00:00"
},
- {
- "name": "laravel/telescope",
- "version": "v5.2.2",
- "source": {
- "type": "git",
- "url": "https://github.com/laravel/telescope.git",
- "reference": "daaf95dee9fab2dd80f59b5f6611c6c0eff44878"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/laravel/telescope/zipball/daaf95dee9fab2dd80f59b5f6611c6c0eff44878",
- "reference": "daaf95dee9fab2dd80f59b5f6611c6c0eff44878",
- "shasum": ""
- },
- "require": {
- "ext-json": "*",
- "laravel/framework": "^8.37|^9.0|^10.0|^11.0",
- "php": "^8.0",
- "symfony/console": "^5.3|^6.0|^7.0",
- "symfony/var-dumper": "^5.0|^6.0|^7.0"
- },
- "require-dev": {
- "ext-gd": "*",
- "guzzlehttp/guzzle": "^6.0|^7.0",
- "laravel/octane": "^1.4|^2.0|dev-develop",
- "orchestra/testbench": "^6.40|^7.37|^8.17|^9.0",
- "phpstan/phpstan": "^1.10",
- "phpunit/phpunit": "^9.0|^10.5"
- },
- "type": "library",
- "extra": {
- "laravel": {
- "providers": [
- "Laravel\\Telescope\\TelescopeServiceProvider"
- ]
- }
- },
- "autoload": {
- "psr-4": {
- "Laravel\\Telescope\\": "src/",
- "Laravel\\Telescope\\Database\\Factories\\": "database/factories/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Taylor Otwell",
- "email": "taylor@laravel.com"
- },
- {
- "name": "Mohamed Said",
- "email": "mohamed@laravel.com"
- }
- ],
- "description": "An elegant debug assistant for the Laravel framework.",
- "keywords": [
- "debugging",
- "laravel",
- "monitoring"
- ],
- "support": {
- "issues": "https://github.com/laravel/telescope/issues",
- "source": "https://github.com/laravel/telescope/tree/v5.2.2"
- },
- "time": "2024-08-26T12:40:52+00:00"
- },
{
"name": "laravel/tinker",
"version": "v2.10.0",
@@ -9105,16 +9113,16 @@
},
{
"name": "symfony/http-foundation",
- "version": "v7.1.5",
+ "version": "v7.1.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
- "reference": "e30ef73b1e44eea7eb37ba69600a354e553f694b"
+ "reference": "5183b61657807099d98f3367bcccb850238b17a9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e30ef73b1e44eea7eb37ba69600a354e553f694b",
- "reference": "e30ef73b1e44eea7eb37ba69600a354e553f694b",
+ "url": "https://api.github.com/repos/symfony/http-foundation/zipball/5183b61657807099d98f3367bcccb850238b17a9",
+ "reference": "5183b61657807099d98f3367bcccb850238b17a9",
"shasum": ""
},
"require": {
@@ -9162,7 +9170,7 @@
"description": "Defines an object-oriented layer for the HTTP specification",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/http-foundation/tree/v7.1.5"
+ "source": "https://github.com/symfony/http-foundation/tree/v7.1.7"
},
"funding": [
{
@@ -9178,7 +9186,7 @@
"type": "tidelift"
}
],
- "time": "2024-09-20T08:28:38+00:00"
+ "time": "2024-11-06T09:02:46+00:00"
},
{
"name": "symfony/http-kernel",
@@ -9376,16 +9384,16 @@
},
{
"name": "symfony/mime",
- "version": "v7.1.5",
+ "version": "v7.1.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
- "reference": "711d2e167e8ce65b05aea6b258c449671cdd38ff"
+ "reference": "caa1e521edb2650b8470918dfe51708c237f0598"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/mime/zipball/711d2e167e8ce65b05aea6b258c449671cdd38ff",
- "reference": "711d2e167e8ce65b05aea6b258c449671cdd38ff",
+ "url": "https://api.github.com/repos/symfony/mime/zipball/caa1e521edb2650b8470918dfe51708c237f0598",
+ "reference": "caa1e521edb2650b8470918dfe51708c237f0598",
"shasum": ""
},
"require": {
@@ -9440,7 +9448,7 @@
"mime-type"
],
"support": {
- "source": "https://github.com/symfony/mime/tree/v7.1.5"
+ "source": "https://github.com/symfony/mime/tree/v7.1.6"
},
"funding": [
{
@@ -9456,7 +9464,7 @@
"type": "tidelift"
}
],
- "time": "2024-09-20T08:28:38+00:00"
+ "time": "2024-10-25T15:11:02+00:00"
},
{
"name": "symfony/options-resolver",
@@ -10243,16 +10251,16 @@
},
{
"name": "symfony/process",
- "version": "v7.1.5",
+ "version": "v7.1.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
- "reference": "5c03ee6369281177f07f7c68252a280beccba847"
+ "reference": "9b8a40b7289767aa7117e957573c2a535efe6585"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/process/zipball/5c03ee6369281177f07f7c68252a280beccba847",
- "reference": "5c03ee6369281177f07f7c68252a280beccba847",
+ "url": "https://api.github.com/repos/symfony/process/zipball/9b8a40b7289767aa7117e957573c2a535efe6585",
+ "reference": "9b8a40b7289767aa7117e957573c2a535efe6585",
"shasum": ""
},
"require": {
@@ -10284,7 +10292,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/process/tree/v7.1.5"
+ "source": "https://github.com/symfony/process/tree/v7.1.7"
},
"funding": [
{
@@ -10300,7 +10308,7 @@
"type": "tidelift"
}
],
- "time": "2024-09-19T21:48:23+00:00"
+ "time": "2024-11-06T09:25:12+00:00"
},
{
"name": "symfony/psr-http-message-bridge",
@@ -12390,6 +12398,75 @@
},
"time": "2024-09-24T17:22:50+00:00"
},
+ {
+ "name": "laravel/telescope",
+ "version": "v5.2.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laravel/telescope.git",
+ "reference": "749369e996611d803e7c1b57929b482dd676008d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laravel/telescope/zipball/749369e996611d803e7c1b57929b482dd676008d",
+ "reference": "749369e996611d803e7c1b57929b482dd676008d",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "laravel/framework": "^8.37|^9.0|^10.0|^11.0",
+ "php": "^8.0",
+ "symfony/console": "^5.3|^6.0|^7.0",
+ "symfony/var-dumper": "^5.0|^6.0|^7.0"
+ },
+ "require-dev": {
+ "ext-gd": "*",
+ "guzzlehttp/guzzle": "^6.0|^7.0",
+ "laravel/octane": "^1.4|^2.0|dev-develop",
+ "orchestra/testbench": "^6.40|^7.37|^8.17|^9.0",
+ "phpstan/phpstan": "^1.10",
+ "phpunit/phpunit": "^9.0|^10.5"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Laravel\\Telescope\\TelescopeServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Laravel\\Telescope\\": "src/",
+ "Laravel\\Telescope\\Database\\Factories\\": "database/factories/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Taylor Otwell",
+ "email": "taylor@laravel.com"
+ },
+ {
+ "name": "Mohamed Said",
+ "email": "mohamed@laravel.com"
+ }
+ ],
+ "description": "An elegant debug assistant for the Laravel framework.",
+ "keywords": [
+ "debugging",
+ "laravel",
+ "monitoring"
+ ],
+ "support": {
+ "issues": "https://github.com/laravel/telescope/issues",
+ "source": "https://github.com/laravel/telescope/tree/v5.2.4"
+ },
+ "time": "2024-10-29T15:35:13+00:00"
+ },
{
"name": "maximebf/debugbar",
"version": "v1.23.2",
@@ -14897,16 +14974,16 @@
},
{
"name": "symfony/http-client",
- "version": "v6.4.12",
+ "version": "v6.4.14",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client.git",
- "reference": "fbebfcce21084d3e91ea987ae5bdd8c71ff0fd56"
+ "reference": "05d88cbd816ad6e0202edd9a9963cb9d615b8826"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-client/zipball/fbebfcce21084d3e91ea987ae5bdd8c71ff0fd56",
- "reference": "fbebfcce21084d3e91ea987ae5bdd8c71ff0fd56",
+ "url": "https://api.github.com/repos/symfony/http-client/zipball/05d88cbd816ad6e0202edd9a9963cb9d615b8826",
+ "reference": "05d88cbd816ad6e0202edd9a9963cb9d615b8826",
"shasum": ""
},
"require": {
@@ -14970,7 +15047,7 @@
"http"
],
"support": {
- "source": "https://github.com/symfony/http-client/tree/v6.4.12"
+ "source": "https://github.com/symfony/http-client/tree/v6.4.14"
},
"funding": [
{
@@ -14986,7 +15063,7 @@
"type": "tidelift"
}
],
- "time": "2024-09-20T08:21:33+00:00"
+ "time": "2024-11-05T16:39:55+00:00"
},
{
"name": "symfony/http-client-contracts",
diff --git a/config/app.php b/config/app.php
index 34484fe41..371ac44ec 100644
--- a/config/app.php
+++ b/config/app.php
@@ -199,8 +199,6 @@ return [
App\Providers\EventServiceProvider::class,
App\Providers\HorizonServiceProvider::class,
App\Providers\RouteServiceProvider::class,
- App\Providers\TelescopeServiceProvider::class,
-
],
/*
diff --git a/config/constants.php b/config/constants.php
index 5792b358c..1bec2e3bf 100644
--- a/config/constants.php
+++ b/config/constants.php
@@ -1,6 +1,7 @@
'26.0',
'docs' => [
'base_url' => 'https://coolify.io/docs',
'contact' => 'https://coolify.io/docs/contact',
@@ -18,7 +19,7 @@ return [
'invitation' => [
'link' => [
'base_url' => '/invitations/',
- 'expiration' => 10,
+ 'expiration_days' => 3,
],
],
'services' => [
diff --git a/config/coolify.php b/config/coolify.php
index f9878fff7..225dfe6fa 100644
--- a/config/coolify.php
+++ b/config/coolify.php
@@ -1,6 +1,7 @@
env('SENTRY_DSN'),
'docs' => 'https://coolify.io/docs/',
'contact' => 'https://coolify.io/docs/contact',
'feedback_discord_webhook' => env('FEEDBACK_DISCORD_WEBHOOK'),
diff --git a/config/debugbar.php b/config/debugbar.php
index eae406ba7..daeea96b6 100644
--- a/config/debugbar.php
+++ b/config/debugbar.php
@@ -18,6 +18,7 @@ return [
'except' => [
'telescope*',
'horizon*',
+ 'api*',
],
/*
diff --git a/config/horizon.php b/config/horizon.php
index 939d74883..6086b30da 100644
--- a/config/horizon.php
+++ b/config/horizon.php
@@ -197,6 +197,7 @@ return [
'production' => [
's6' => [
'autoScalingStrategy' => 'size',
+ 'minProcesses' => env('HORIZON_MIN_PROCESSES', 1),
'maxProcesses' => env('HORIZON_MAX_PROCESSES', 6),
'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1),
'balanceCooldown' => env('HORIZON_BALANCE_COOLDOWN', 1),
@@ -206,6 +207,7 @@ return [
'local' => [
's6' => [
'autoScalingStrategy' => 'size',
+ 'minProcesses' => env('HORIZON_MIN_PROCESSES', 1),
'maxProcesses' => env('HORIZON_MAX_PROCESSES', 6),
'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1),
'balanceCooldown' => env('HORIZON_BALANCE_COOLDOWN', 1),
diff --git a/config/sentry.php b/config/sentry.php
index ade6923ac..8ff3c354a 100644
--- a/config/sentry.php
+++ b/config/sentry.php
@@ -3,11 +3,11 @@
return [
// @see https://docs.sentry.io/product/sentry-basics/dsn-explainer/
- 'dsn' => 'https://89552af6db48f4ca6a871ec0fc42964d@o1082494.ingest.us.sentry.io/4505347448045568',
+ 'dsn' => config('coolify.sentry_dsn'),
// 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.360',
+ 'release' => '4.0.0-beta.363',
// When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'),
diff --git a/config/telescope.php b/config/telescope.php
index 24077c24d..c940bec8a 100644
--- a/config/telescope.php
+++ b/config/telescope.php
@@ -76,8 +76,8 @@ return [
*/
'queue' => [
- 'connection' => env('TELESCOPE_QUEUE_CONNECTION', null),
- 'queue' => env('TELESCOPE_QUEUE', null),
+ 'connection' => env('TELESCOPE_QUEUE_CONNECTION', 'redis'),
+ 'queue' => env('TELESCOPE_QUEUE', 'default'),
],
/*
@@ -115,7 +115,6 @@ return [
'livewire*',
'nova-api*',
'pulse*',
- 'broadcasting/auth',
],
'ignore_commands' => [
@@ -161,20 +160,20 @@ return [
Watchers\ExceptionWatcher::class => env('TELESCOPE_EXCEPTION_WATCHER', true),
Watchers\GateWatcher::class => [
- 'enabled' => env('TELESCOPE_GATE_WATCHER', false),
+ 'enabled' => env('TELESCOPE_GATE_WATCHER', true),
'ignore_abilities' => [],
'ignore_packages' => true,
'ignore_paths' => [],
],
- Watchers\JobWatcher::class => env('TELESCOPE_JOB_WATCHER', false),
+ Watchers\JobWatcher::class => env('TELESCOPE_JOB_WATCHER', true),
Watchers\LogWatcher::class => [
'enabled' => env('TELESCOPE_LOG_WATCHER', true),
- 'level' => 'debug',
+ 'level' => 'error',
],
- Watchers\MailWatcher::class => env('TELESCOPE_MAIL_WATCHER', false),
+ Watchers\MailWatcher::class => env('TELESCOPE_MAIL_WATCHER', true),
Watchers\ModelWatcher::class => [
'enabled' => env('TELESCOPE_MODEL_WATCHER', true),
@@ -182,7 +181,7 @@ return [
'hydrations' => true,
],
- Watchers\NotificationWatcher::class => env('TELESCOPE_NOTIFICATION_WATCHER', false),
+ Watchers\NotificationWatcher::class => env('TELESCOPE_NOTIFICATION_WATCHER', true),
Watchers\QueryWatcher::class => [
'enabled' => env('TELESCOPE_QUERY_WATCHER', true),
@@ -200,7 +199,7 @@ return [
'ignore_status_codes' => [],
],
- Watchers\ScheduleWatcher::class => env('TELESCOPE_SCHEDULE_WATCHER', false),
+ Watchers\ScheduleWatcher::class => env('TELESCOPE_SCHEDULE_WATCHER', true),
Watchers\ViewWatcher::class => env('TELESCOPE_VIEW_WATCHER', true),
],
];
diff --git a/config/testing.php b/config/testing.php
new file mode 100644
index 000000000..41b8eadf0
--- /dev/null
+++ b/config/testing.php
@@ -0,0 +1,6 @@
+ env('DUSK_TEST_EMAIL', 'test@example.com'),
+ 'dusk_test_password' => env('DUSK_TEST_PASSWORD', 'password'),
+];
diff --git a/config/version.php b/config/version.php
index 5639fc8a8..abee59dbc 100644
--- a/config/version.php
+++ b/config/version.php
@@ -1,3 +1,3 @@
string('stripe_plan_id')->nullable()->after('stripe_cancel_at_period_end');
-
});
}
diff --git a/database/migrations/2023_08_22_071054_add_stripe_reasons.php b/database/migrations/2023_08_22_071054_add_stripe_reasons.php
index efd611aac..6ffe37e98 100644
--- a/database/migrations/2023_08_22_071054_add_stripe_reasons.php
+++ b/database/migrations/2023_08_22_071054_add_stripe_reasons.php
@@ -14,7 +14,6 @@ return new class extends Migration
Schema::table('subscriptions', function (Blueprint $table) {
$table->string('stripe_feedback')->nullable()->after('stripe_cancel_at_period_end');
$table->string('stripe_comment')->nullable()->after('stripe_feedback');
-
});
}
diff --git a/database/migrations/2023_08_22_071059_add_stripe_trial_ended.php b/database/migrations/2023_08_22_071059_add_stripe_trial_ended.php
index c22317e6b..61fcbda6b 100644
--- a/database/migrations/2023_08_22_071059_add_stripe_trial_ended.php
+++ b/database/migrations/2023_08_22_071059_add_stripe_trial_ended.php
@@ -13,7 +13,6 @@ return new class extends Migration
{
Schema::table('subscriptions', function (Blueprint $table) {
$table->boolean('stripe_trial_already_ended')->default(false)->after('stripe_cancel_at_period_end');
-
});
}
diff --git a/database/migrations/2023_08_22_071060_change_invitation_link_length.php b/database/migrations/2023_08_22_071060_change_invitation_link_length.php
index 4efb03351..9d14c3f26 100644
--- a/database/migrations/2023_08_22_071060_change_invitation_link_length.php
+++ b/database/migrations/2023_08_22_071060_change_invitation_link_length.php
@@ -13,7 +13,6 @@ return new class extends Migration
{
Schema::table('team_invitations', function (Blueprint $table) {
$table->text('link')->change();
-
});
}
diff --git a/database/migrations/2023_09_20_082541_update_services_table.php b/database/migrations/2023_09_20_082541_update_services_table.php
index 8c6b350f7..c70cd28f7 100644
--- a/database/migrations/2023_09_20_082541_update_services_table.php
+++ b/database/migrations/2023_09_20_082541_update_services_table.php
@@ -16,7 +16,6 @@ return new class extends Migration
$table->longText('description')->nullable();
$table->longText('docker_compose_raw');
$table->longText('docker_compose')->nullable();
-
});
}
diff --git a/database/migrations/2023_09_20_083549_update_environment_variables_table.php b/database/migrations/2023_09_20_083549_update_environment_variables_table.php
index 40eb6aa44..a96d096bb 100644
--- a/database/migrations/2023_09_20_083549_update_environment_variables_table.php
+++ b/database/migrations/2023_09_20_083549_update_environment_variables_table.php
@@ -13,7 +13,6 @@ return new class extends Migration
{
Schema::table('environment_variables', function (Blueprint $table) {
$table->foreignId('service_id')->nullable();
-
});
}
diff --git a/database/migrations/2023_09_23_111809_remove_destination_from_services_table.php b/database/migrations/2023_09_23_111809_remove_destination_from_services_table.php
index 920f44a72..729146a4a 100644
--- a/database/migrations/2023_09_23_111809_remove_destination_from_services_table.php
+++ b/database/migrations/2023_09_23_111809_remove_destination_from_services_table.php
@@ -14,7 +14,6 @@ return new class extends Migration
Schema::table('services', function (Blueprint $table) {
$table->dropColumn('destination_type');
$table->dropColumn('destination_id');
-
});
}
diff --git a/database/migrations/2023_09_23_111819_add_server_emails.php b/database/migrations/2023_09_23_111819_add_server_emails.php
index 03c1e6bd2..775e82010 100644
--- a/database/migrations/2023_09_23_111819_add_server_emails.php
+++ b/database/migrations/2023_09_23_111819_add_server_emails.php
@@ -26,6 +26,5 @@ return new class extends Migration
$table->dropColumn('unreachable_email_sent');
$table->integer('unreachable_count')->default(0);
});
-
}
};
diff --git a/database/migrations/2023_11_16_220647_add_log_drains.php b/database/migrations/2023_11_16_220647_add_log_drains.php
index 05b1ed054..f5161b3d7 100644
--- a/database/migrations/2023_11_16_220647_add_log_drains.php
+++ b/database/migrations/2023_11_16_220647_add_log_drains.php
@@ -22,7 +22,6 @@ return new class extends Migration
$table->boolean('is_logdrain_axiom_enabled')->default(false);
$table->string('logdrain_axiom_dataset_name')->nullable();
$table->string('logdrain_axiom_api_key')->nullable();
-
});
}
diff --git a/database/migrations/2023_12_13_110214_add_soft_deletes.php b/database/migrations/2023_12_13_110214_add_soft_deletes.php
index ab7b562b4..72350b77f 100644
--- a/database/migrations/2023_12_13_110214_add_soft_deletes.php
+++ b/database/migrations/2023_12_13_110214_add_soft_deletes.php
@@ -66,6 +66,5 @@ return new class extends Migration
Schema::table('service_databases', function (Blueprint $table) {
$table->dropSoftDeletes();
});
-
}
};
diff --git a/database/migrations/2023_12_17_155616_add_custom_docker_compose_start_command.php b/database/migrations/2023_12_17_155616_add_custom_docker_compose_start_command.php
index eeb2769fe..f28b2670e 100644
--- a/database/migrations/2023_12_17_155616_add_custom_docker_compose_start_command.php
+++ b/database/migrations/2023_12_17_155616_add_custom_docker_compose_start_command.php
@@ -14,7 +14,6 @@ return new class extends Migration
Schema::table('applications', function (Blueprint $table) {
$table->string('docker_compose_custom_start_command')->nullable();
$table->string('docker_compose_custom_build_command')->nullable();
-
});
}
diff --git a/database/migrations/2024_04_09_095517_make_custom_docker_commands_longer.php b/database/migrations/2024_04_09_095517_make_custom_docker_commands_longer.php
index 7df53ec06..b3f2c1920 100644
--- a/database/migrations/2024_04_09_095517_make_custom_docker_commands_longer.php
+++ b/database/migrations/2024_04_09_095517_make_custom_docker_commands_longer.php
@@ -13,7 +13,6 @@ return new class extends Migration
{
Schema::table('applications', function (Blueprint $table) {
$table->text('custom_docker_run_options')->nullable()->change();
-
});
}
diff --git a/database/migrations/2024_04_25_073615_add_docker_network_to_application_settings.php b/database/migrations/2024_04_25_073615_add_docker_network_to_application_settings.php
index aeae6f77d..bea410eab 100644
--- a/database/migrations/2024_04_25_073615_add_docker_network_to_application_settings.php
+++ b/database/migrations/2024_04_25_073615_add_docker_network_to_application_settings.php
@@ -13,7 +13,6 @@ return new class extends Migration
{
Schema::table('application_settings', function (Blueprint $table) {
$table->boolean('connect_to_docker_network')->default(false);
-
});
}
diff --git a/database/migrations/2024_05_23_091713_add_gitea_webhook_to_applications.php b/database/migrations/2024_05_23_091713_add_gitea_webhook_to_applications.php
index 716f1f44c..b46a07203 100644
--- a/database/migrations/2024_05_23_091713_add_gitea_webhook_to_applications.php
+++ b/database/migrations/2024_05_23_091713_add_gitea_webhook_to_applications.php
@@ -13,7 +13,6 @@ return new class extends Migration
{
Schema::table('applications', function (Blueprint $table) {
$table->string('manual_webhook_secret_gitea')->nullable();
-
});
}
diff --git a/database/migrations/2024_06_18_105948_move_server_metrics.php b/database/migrations/2024_06_18_105948_move_server_metrics.php
index 26a1d1684..a6bccd16a 100644
--- a/database/migrations/2024_06_18_105948_move_server_metrics.php
+++ b/database/migrations/2024_06_18_105948_move_server_metrics.php
@@ -18,7 +18,7 @@ return new class extends Migration
$table->boolean('is_metrics_enabled')->default(false);
$table->integer('metrics_refresh_rate_seconds')->default(5);
$table->integer('metrics_history_days')->default(30);
- $table->string('metrics_token')->default(generateSentinelToken());
+ $table->string('metrics_token')->nullable();
});
}
diff --git a/database/migrations/2024_06_25_184323_update_db.php b/database/migrations/2024_06_25_184323_update_db.php
index f1b175a9c..d9cddb15f 100644
--- a/database/migrations/2024_06_25_184323_update_db.php
+++ b/database/migrations/2024_06_25_184323_update_db.php
@@ -4,6 +4,8 @@ use App\Models\EnvironmentVariable;
use App\Models\Server;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;
use Visus\Cuid2\Cuid2;
@@ -14,44 +16,45 @@ return new class extends Migration
*/
public function up(): void
{
- Schema::table('applications', function (Blueprint $table) {
- $table->dropColumn('docker_compose_pr_location');
- $table->dropColumn('docker_compose_pr');
- $table->dropColumn('docker_compose_pr_raw');
- });
- Schema::table('subscriptions', function (Blueprint $table) {
- $table->dropColumn('lemon_subscription_id');
- $table->dropColumn('lemon_order_id');
- $table->dropColumn('lemon_product_id');
- $table->dropColumn('lemon_variant_id');
- $table->dropColumn('lemon_variant_name');
- $table->dropColumn('lemon_customer_id');
- $table->dropColumn('lemon_status');
- $table->dropColumn('lemon_renews_at');
- $table->dropColumn('lemon_update_payment_menthod_url');
- $table->dropColumn('lemon_trial_ends_at');
- $table->dropColumn('lemon_ends_at');
- });
- Schema::table('environment_variables', function (Blueprint $table) {
- $table->string('uuid')->nullable()->after('id');
- });
+ try {
+ Schema::table('applications', function (Blueprint $table) {
+ $table->dropColumn('docker_compose_pr_location');
+ $table->dropColumn('docker_compose_pr');
+ $table->dropColumn('docker_compose_pr_raw');
+ });
+ Schema::table('subscriptions', function (Blueprint $table) {
+ $table->dropColumn('lemon_subscription_id');
+ $table->dropColumn('lemon_order_id');
+ $table->dropColumn('lemon_product_id');
+ $table->dropColumn('lemon_variant_id');
+ $table->dropColumn('lemon_variant_name');
+ $table->dropColumn('lemon_customer_id');
+ $table->dropColumn('lemon_status');
+ $table->dropColumn('lemon_renews_at');
+ $table->dropColumn('lemon_update_payment_menthod_url');
+ $table->dropColumn('lemon_trial_ends_at');
+ $table->dropColumn('lemon_ends_at');
+ });
+ Schema::table('environment_variables', function (Blueprint $table) {
+ $table->string('uuid')->nullable()->after('id');
+ });
- EnvironmentVariable::all()->each(function (EnvironmentVariable $environmentVariable) {
- $environmentVariable->update([
- 'uuid' => (string) new Cuid2,
- ]);
- });
- Schema::table('environment_variables', function (Blueprint $table) {
- $table->string('uuid')->nullable(false)->change();
- });
- Schema::table('server_settings', function (Blueprint $table) {
- $table->integer('metrics_history_days')->default(7)->change();
- });
- Server::all()->each(function (Server $server) {
- $server->settings->update([
- 'metrics_history_days' => 7,
- ]);
- });
+ EnvironmentVariable::all()->each(function (EnvironmentVariable $environmentVariable) {
+ $environmentVariable->update([
+ 'uuid' => (string) new Cuid2,
+ ]);
+ });
+ Schema::table('environment_variables', function (Blueprint $table) {
+ $table->string('uuid')->nullable(false)->change();
+ });
+ Schema::table('server_settings', function (Blueprint $table) {
+ $table->integer('metrics_history_days')->default(7)->change();
+ });
+
+ DB::table('server_settings')->update(['metrics_history_days' => 7]);
+ } catch (\Exception $e) {
+ Log::error('Error updating db: '.$e->getMessage());
+ }
}
/**
diff --git a/database/migrations/2024_07_18_123458_add_force_cleanup_server.php b/database/migrations/2024_07_18_123458_add_force_cleanup_server.php
index a33665bd0..ea3695b3f 100644
--- a/database/migrations/2024_07_18_123458_add_force_cleanup_server.php
+++ b/database/migrations/2024_07_18_123458_add_force_cleanup_server.php
@@ -12,7 +12,7 @@ return new class extends Migration
public function up(): void
{
Schema::table('server_settings', function (Blueprint $table) {
- $table->boolean('is_force_cleanup_enabled')->default(false)->after('is_sentinel_enabled');
+ $table->boolean('is_force_cleanup_enabled')->default(false);
});
}
diff --git a/database/migrations/2024_08_09_215659_add_server_cleanup_fields_to_server_settings_table.php b/database/migrations/2024_08_09_215659_add_server_cleanup_fields_to_server_settings_table.php
index b5300c905..e3bdc68c6 100644
--- a/database/migrations/2024_08_09_215659_add_server_cleanup_fields_to_server_settings_table.php
+++ b/database/migrations/2024_08_09_215659_add_server_cleanup_fields_to_server_settings_table.php
@@ -30,7 +30,6 @@ class AddServerCleanupFieldsToServerSettingsTable extends Migration
$serverSetting->docker_cleanup_threshold = $serverSetting->cleanup_after_percentage;
$serverSetting->save();
}
-
}
/**
diff --git a/database/migrations/2024_09_16_111428_encrypt_existing_private_keys.php b/database/migrations/2024_09_16_111428_encrypt_existing_private_keys.php
index 19274ad9b..e16181ac7 100644
--- a/database/migrations/2024_09_16_111428_encrypt_existing_private_keys.php
+++ b/database/migrations/2024_09_16_111428_encrypt_existing_private_keys.php
@@ -20,6 +20,5 @@ class EncryptExistingPrivateKeys extends Migration
echo 'Encrypting private keys failed.';
echo $e->getMessage();
}
-
}
}
diff --git a/database/migrations/2024_10_11_114331_add_required_env_variables.php b/database/migrations/2024_10_11_114331_add_required_env_variables.php
new file mode 100644
index 000000000..4fde0c2bb
--- /dev/null
+++ b/database/migrations/2024_10_11_114331_add_required_env_variables.php
@@ -0,0 +1,28 @@
+boolean('is_required')->default(false);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('environment_variables', function (Blueprint $table) {
+ $table->dropColumn('is_required');
+ });
+ }
+};
diff --git a/database/migrations/2024_10_14_090416_update_metrics_token_in_server_settings.php b/database/migrations/2024_10_14_090416_update_metrics_token_in_server_settings.php
new file mode 100644
index 000000000..d5c38501f
--- /dev/null
+++ b/database/migrations/2024_10_14_090416_update_metrics_token_in_server_settings.php
@@ -0,0 +1,54 @@
+dropColumn('metrics_token');
+ $table->dropColumn('metrics_refresh_rate_seconds');
+ $table->dropColumn('metrics_history_days');
+ $table->dropColumn('is_server_api_enabled');
+
+ $table->boolean('is_sentinel_enabled')->default(false);
+ $table->text('sentinel_token')->nullable();
+ $table->integer('sentinel_metrics_refresh_rate_seconds')->default(10);
+ $table->integer('sentinel_metrics_history_days')->default(7);
+ $table->integer('sentinel_push_interval_seconds')->default(60);
+ $table->string('sentinel_custom_url')->nullable();
+ });
+ Schema::table('servers', function (Blueprint $table) {
+ $table->dateTime('sentinel_updated_at')->default(now());
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('server_settings', function (Blueprint $table) {
+ $table->string('metrics_token')->nullable();
+ $table->integer('metrics_refresh_rate_seconds')->default(5);
+ $table->integer('metrics_history_days')->default(30);
+ $table->boolean('is_server_api_enabled')->default(false);
+
+ $table->dropColumn('is_sentinel_enabled');
+ $table->dropColumn('sentinel_token');
+ $table->dropColumn('sentinel_metrics_refresh_rate_seconds');
+ $table->dropColumn('sentinel_metrics_history_days');
+ $table->dropColumn('sentinel_push_interval_seconds');
+ $table->dropColumn('sentinel_custom_url');
+ });
+ Schema::table('servers', function (Blueprint $table) {
+ $table->dropColumn('sentinel_updated_at');
+ });
+ }
+};
diff --git a/database/migrations/2024_10_15_172139_add_is_shared_to_environment_variables.php b/database/migrations/2024_10_15_172139_add_is_shared_to_environment_variables.php
new file mode 100644
index 000000000..eb878e2f6
--- /dev/null
+++ b/database/migrations/2024_10_15_172139_add_is_shared_to_environment_variables.php
@@ -0,0 +1,22 @@
+boolean('is_shared')->default(false);
+ });
+ }
+
+ public function down()
+ {
+ Schema::table('environment_variables', function (Blueprint $table) {
+ $table->dropColumn('is_shared');
+ });
+ }
+}
diff --git a/database/migrations/2024_10_16_120026_move_redis_password_to_envs.php b/database/migrations/2024_10_16_120026_move_redis_password_to_envs.php
new file mode 100644
index 000000000..fa01e8e85
--- /dev/null
+++ b/database/migrations/2024_10_16_120026_move_redis_password_to_envs.php
@@ -0,0 +1,41 @@
+where('id', $redis->id)->value('redis_password');
+ EnvironmentVariable::create([
+ 'standalone_redis_id' => $redis->id,
+ 'key' => 'REDIS_PASSWORD',
+ 'value' => $redis_password,
+ ]);
+ EnvironmentVariable::create([
+ 'standalone_redis_id' => $redis->id,
+ 'key' => 'REDIS_USERNAME',
+ 'value' => 'default',
+ ]);
+ }
+ });
+ Schema::table('standalone_redis', function (Blueprint $table) {
+ $table->dropColumn('redis_password');
+ });
+ } catch (\Exception $e) {
+ echo 'Moving Redis passwords to envs failed.';
+ echo $e->getMessage();
+ }
+ }
+}
diff --git a/database/migrations/2024_10_16_192133_add_confirmation_settings_to_instance_settings_table.php b/database/migrations/2024_10_16_192133_add_confirmation_settings_to_instance_settings_table.php
new file mode 100644
index 000000000..7040daf44
--- /dev/null
+++ b/database/migrations/2024_10_16_192133_add_confirmation_settings_to_instance_settings_table.php
@@ -0,0 +1,22 @@
+boolean('disable_two_step_confirmation')->default(false);
+ });
+ }
+
+ public function down()
+ {
+ Schema::table('instance_settings', function (Blueprint $table) {
+ $table->dropColumn('disable_two_step_confirmation');
+ });
+ }
+};
diff --git a/database/migrations/2024_10_17_093722_add_soft_delete_to_servers.php b/database/migrations/2024_10_17_093722_add_soft_delete_to_servers.php
new file mode 100644
index 000000000..7a7f28e24
--- /dev/null
+++ b/database/migrations/2024_10_17_093722_add_soft_delete_to_servers.php
@@ -0,0 +1,28 @@
+softDeletes();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('servers', function (Blueprint $table) {
+ $table->dropSoftDeletes();
+ });
+ }
+};
diff --git a/database/migrations/2024_10_22_105745_add_server_disk_usage_threshold.php b/database/migrations/2024_10_22_105745_add_server_disk_usage_threshold.php
new file mode 100644
index 000000000..76ccd1352
--- /dev/null
+++ b/database/migrations/2024_10_22_105745_add_server_disk_usage_threshold.php
@@ -0,0 +1,28 @@
+integer('server_disk_usage_notification_threshold')->default(80)->after('docker_cleanup_threshold');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('server_settings', function (Blueprint $table) {
+ $table->dropColumn('server_disk_usage_notification_threshold');
+ });
+ }
+};
diff --git a/database/migrations/2024_10_22_121223_add_server_disk_usage_notification.php b/database/migrations/2024_10_22_121223_add_server_disk_usage_notification.php
new file mode 100644
index 000000000..a2aa381b7
--- /dev/null
+++ b/database/migrations/2024_10_22_121223_add_server_disk_usage_notification.php
@@ -0,0 +1,32 @@
+boolean('discord_notifications_server_disk_usage')->default(true)->after('discord_enabled');
+ $table->boolean('smtp_notifications_server_disk_usage')->default(true)->after('smtp_enabled');
+ $table->boolean('telegram_notifications_server_disk_usage')->default(true)->after('telegram_enabled');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('teams', function (Blueprint $table) {
+ $table->dropColumn('discord_notifications_server_disk_usage');
+ $table->dropColumn('smtp_notifications_server_disk_usage');
+ $table->dropColumn('telegram_notifications_server_disk_usage');
+ });
+ }
+};
diff --git a/database/migrations/2024_10_29_093927_add_is_sentinel_debug_enabled_to_server_settings.php b/database/migrations/2024_10_29_093927_add_is_sentinel_debug_enabled_to_server_settings.php
new file mode 100644
index 000000000..d8ab1313b
--- /dev/null
+++ b/database/migrations/2024_10_29_093927_add_is_sentinel_debug_enabled_to_server_settings.php
@@ -0,0 +1,28 @@
+boolean('is_sentinel_debug_enabled')->default(false);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('server_settings', function (Blueprint $table) {
+ $table->dropColumn('is_sentinel_debug_enabled');
+ });
+ }
+};
diff --git a/database/migrations/2024_11_02_213214_add_last_online_at_to_resources.php b/database/migrations/2024_11_02_213214_add_last_online_at_to_resources.php
new file mode 100644
index 000000000..51b8fb3ba
--- /dev/null
+++ b/database/migrations/2024_11_02_213214_add_last_online_at_to_resources.php
@@ -0,0 +1,96 @@
+timestamp('last_online_at')->default(now())->after('updated_at');
+ });
+ Schema::table('application_previews', function (Blueprint $table) {
+ $table->timestamp('last_online_at')->default(now())->after('updated_at');
+ });
+ Schema::table('service_applications', function (Blueprint $table) {
+ $table->timestamp('last_online_at')->default(now())->after('updated_at');
+ });
+ Schema::table('service_databases', function (Blueprint $table) {
+ $table->timestamp('last_online_at')->default(now())->after('updated_at');
+ });
+ Schema::table('standalone_postgresqls', function (Blueprint $table) {
+ $table->timestamp('last_online_at')->default(now())->after('updated_at');
+ });
+ Schema::table('standalone_redis', function (Blueprint $table) {
+ $table->timestamp('last_online_at')->default(now())->after('updated_at');
+ });
+ Schema::table('standalone_mongodbs', function (Blueprint $table) {
+ $table->timestamp('last_online_at')->default(now())->after('updated_at');
+ });
+ Schema::table('standalone_mysqls', function (Blueprint $table) {
+ $table->timestamp('last_online_at')->default(now())->after('updated_at');
+ });
+ Schema::table('standalone_mariadbs', function (Blueprint $table) {
+ $table->timestamp('last_online_at')->default(now())->after('updated_at');
+ });
+ Schema::table('standalone_keydbs', function (Blueprint $table) {
+ $table->timestamp('last_online_at')->default(now())->after('updated_at');
+ });
+ Schema::table('standalone_dragonflies', function (Blueprint $table) {
+ $table->timestamp('last_online_at')->default(now())->after('updated_at');
+ });
+ Schema::table('standalone_clickhouses', function (Blueprint $table) {
+ $table->timestamp('last_online_at')->default(now())->after('updated_at');
+ });
+
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('applications', function (Blueprint $table) {
+ $table->dropColumn('last_online_at');
+ });
+ Schema::table('application_previews', function (Blueprint $table) {
+ $table->dropColumn('last_online_at');
+ });
+ Schema::table('service_applications', function (Blueprint $table) {
+ $table->dropColumn('last_online_at');
+ });
+ Schema::table('service_databases', function (Blueprint $table) {
+ $table->dropColumn('last_online_at');
+ });
+ Schema::table('standalone_postgresqls', function (Blueprint $table) {
+ $table->dropColumn('last_online_at');
+ });
+ Schema::table('standalone_redis', function (Blueprint $table) {
+ $table->dropColumn('last_online_at');
+ });
+ Schema::table('standalone_mongodbs', function (Blueprint $table) {
+ $table->dropColumn('last_online_at');
+ });
+ Schema::table('standalone_mysqls', function (Blueprint $table) {
+ $table->dropColumn('last_online_at');
+ });
+ Schema::table('standalone_mariadbs', function (Blueprint $table) {
+ $table->dropColumn('last_online_at');
+ });
+ Schema::table('standalone_keydbs', function (Blueprint $table) {
+ $table->dropColumn('last_online_at');
+ });
+ Schema::table('standalone_dragonflies', function (Blueprint $table) {
+ $table->dropColumn('last_online_at');
+ });
+ Schema::table('standalone_clickhouses', function (Blueprint $table) {
+ $table->dropColumn('last_online_at');
+ });
+
+ }
+};
diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php
index be5083108..6e66c64f4 100644
--- a/database/seeders/DatabaseSeeder.php
+++ b/database/seeders/DatabaseSeeder.php
@@ -26,6 +26,8 @@ class DatabaseSeeder extends Seeder
S3StorageSeeder::class,
StandalonePostgresqlSeeder::class,
OauthSettingSeeder::class,
+ DisableTwoStepConfirmationSeeder::class,
+ SentinelSeeder::class,
]);
}
}
diff --git a/database/seeders/DisableTwoStepConfirmationSeeder.php b/database/seeders/DisableTwoStepConfirmationSeeder.php
new file mode 100644
index 000000000..c43bf1b01
--- /dev/null
+++ b/database/seeders/DisableTwoStepConfirmationSeeder.php
@@ -0,0 +1,20 @@
+updateOrInsert(
+ [],
+ ['disable_two_step_confirmation' => true]
+ );
+ }
+}
diff --git a/database/seeders/PopulateSshKeysDirectorySeeder.php b/database/seeders/PopulateSshKeysDirectorySeeder.php
index e2543ee02..d528179c0 100644
--- a/database/seeders/PopulateSshKeysDirectorySeeder.php
+++ b/database/seeders/PopulateSshKeysDirectorySeeder.php
@@ -33,7 +33,6 @@ class PopulateSshKeysDirectorySeeder extends Seeder
}
} catch (\Throwable $e) {
echo "Error: {$e->getMessage()}\n";
- ray($e->getMessage());
}
}
}
diff --git a/database/seeders/ProductionSeeder.php b/database/seeders/ProductionSeeder.php
index 206f04d6b..3e820a162 100644
--- a/database/seeders/ProductionSeeder.php
+++ b/database/seeders/ProductionSeeder.php
@@ -126,7 +126,6 @@ class ProductionSeeder extends Seeder
echo "Your localhost connection won't work until then.";
}
}
-
}
if (config('coolify.is_windows_docker_desktop')) {
PrivateKey::updateOrCreate(
@@ -186,6 +185,6 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$this->call(OauthSettingSeeder::class);
$this->call(PopulateSshKeysDirectorySeeder::class);
-
+ $this->call(SentinelSeeder::class);
}
}
diff --git a/database/seeders/SentinelSeeder.php b/database/seeders/SentinelSeeder.php
new file mode 100644
index 000000000..3cf913933
--- /dev/null
+++ b/database/seeders/SentinelSeeder.php
@@ -0,0 +1,32 @@
+settings->sentinel_token)->isEmpty()) {
+ $server->settings->generateSentinelToken(ignoreEvent: true);
+ }
+ if (str($server->settings->sentinel_custom_url)->isEmpty()) {
+ $url = $server->settings->generateSentinelUrl(ignoreEvent: true);
+ if (str($url)->isEmpty()) {
+ $server->settings->is_sentinel_enabled = false;
+ $server->settings->save();
+ }
+ }
+ } catch (\Throwable $e) {
+ Log::error('Error seeding sentinel: '.$e->getMessage());
+ }
+ }
+ });
+ }
+}
diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml
index b15a109c3..80555e377 100644
--- a/docker-compose.prod.yml
+++ b/docker-compose.prod.yml
@@ -113,7 +113,7 @@ services:
retries: 10
timeout: 2s
soketi:
- image: 'ghcr.io/coollabsio/coolify-realtime:1.0.3'
+ image: 'ghcr.io/coollabsio/coolify-realtime:1.0.4'
ports:
- "${SOKETI_PORT:-6001}:6001"
- "6002:6002"
diff --git a/docker-compose.windows.yml b/docker-compose.windows.yml
index ef2de82e9..d92dc6332 100644
--- a/docker-compose.windows.yml
+++ b/docker-compose.windows.yml
@@ -103,7 +103,7 @@ services:
retries: 10
timeout: 2s
soketi:
- image: 'ghcr.io/coollabsio/coolify-realtime:1.0.0'
+ image: 'ghcr.io/coollabsio/coolify-realtime:1.0.4'
pull_policy: always
container_name: coolify-realtime
restart: always
diff --git a/docker/coolify-helper/Dockerfile b/docker/coolify-helper/Dockerfile
index 7aa9d8722..48f401da4 100644
--- a/docker/coolify-helper/Dockerfile
+++ b/docker/coolify-helper/Dockerfile
@@ -10,7 +10,7 @@ ARG DOCKER_BUILDX_VERSION=0.14.1
# https://github.com/buildpacks/pack/releases
ARG PACK_VERSION=0.35.1
# https://github.com/railwayapp/nixpacks/releases
-ARG NIXPACKS_VERSION=1.28.0
+ARG NIXPACKS_VERSION=1.29.0
USER root
WORKDIR /artifacts
diff --git a/docker/coolify-realtime/Dockerfile b/docker/coolify-realtime/Dockerfile
index f0d6db906..9f94518f5 100644
--- a/docker/coolify-realtime/Dockerfile
+++ b/docker/coolify-realtime/Dockerfile
@@ -22,6 +22,4 @@ RUN /bin/sh -c "if [[ ${TARGETPLATFORM} == 'linux/arm64' ]]; then \
curl -L https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \
;fi"
-
-
ENTRYPOINT ["/bin/sh", "/soketi-entrypoint.sh"]
diff --git a/docker/coolify-realtime/package-lock.json b/docker/coolify-realtime/package-lock.json
new file mode 100644
index 000000000..f5fd1ba18
--- /dev/null
+++ b/docker/coolify-realtime/package-lock.json
@@ -0,0 +1,190 @@
+{
+ "name": "coolify-realtime",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "dependencies": {
+ "@xterm/addon-fit": "0.10.0",
+ "@xterm/xterm": "5.5.0",
+ "axios": "1.7.5",
+ "cookie": "1.0.1",
+ "dotenv": "16.4.5",
+ "node-pty": "1.0.0",
+ "ws": "8.18.0"
+ }
+ },
+ "node_modules/@xterm/addon-fit": {
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz",
+ "integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@xterm/xterm": "^5.0.0"
+ }
+ },
+ "node_modules/@xterm/xterm": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
+ "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
+ "license": "MIT"
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/axios": {
+ "version": "1.7.5",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.5.tgz",
+ "integrity": "sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.1.tgz",
+ "integrity": "sha512-Xd8lFX4LM9QEEwxQpF9J9NTUh8pmdJO0cyRJhFiDoLTk2eH8FXlRv2IFGYVadZpqI3j8fhNrSdKCeYPxiAhLXw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "16.4.5",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
+ "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.9",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
+ "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
+ "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/nan": {
+ "version": "2.22.0",
+ "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz",
+ "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==",
+ "license": "MIT"
+ },
+ "node_modules/node-pty": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz",
+ "integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "nan": "^2.17.0"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
+ "node_modules/ws": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
+ "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/docker/coolify-realtime/package.json b/docker/coolify-realtime/package.json
index 90d4f77db..faeb80f54 100644
--- a/docker/coolify-realtime/package.json
+++ b/docker/coolify-realtime/package.json
@@ -2,12 +2,12 @@
"private": true,
"type": "module",
"dependencies": {
- "@xterm/addon-fit": "^0.10.0",
- "@xterm/xterm": "^5.5.0",
- "cookie": "^0.6.0",
+ "@xterm/addon-fit": "0.10.0",
+ "@xterm/xterm": "5.5.0",
+ "cookie": "1.0.1",
"axios": "1.7.5",
- "dotenv": "^16.4.5",
- "node-pty": "^1.0.0",
- "ws": "^8.17.0"
+ "dotenv": "16.4.5",
+ "node-pty": "1.0.0",
+ "ws": "8.18.0"
}
}
diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile
index 63832dc36..d2381f764 100644
--- a/docker/dev/Dockerfile
+++ b/docker/dev/Dockerfile
@@ -5,34 +5,38 @@ ARG TARGETPLATFORM
ARG CLOUDFLARED_VERSION=2024.4.1
ARG POSTGRES_VERSION=15
-RUN apt-get update
-# Postgres version requirements
-RUN apt install dirmngr ca-certificates software-properties-common gnupg gnupg2 apt-transport-https curl -y
-RUN curl -fSsL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/postgresql.gpg > /dev/null
-RUN echo deb [arch=amd64,arm64,ppc64el signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ jammy-pgdg main | tee -a /etc/apt/sources.list.d/postgresql.list
+# Use build arguments for caching
+ARG BUILDTIME_DEPS="dirmngr ca-certificates software-properties-common gnupg gnupg2 apt-transport-https curl"
+ARG RUNTIME_DEPS="postgresql-client-$POSTGRES_VERSION php8.2-pgsql openssh-client git git-lfs jq lsof"
-RUN apt-get update
-RUN apt-get install postgresql-client-$POSTGRES_VERSION -y
+# Install dependencies
+RUN --mount=type=cache,target=/var/cache/apt \
+ apt-get update && \
+ apt-get install -y $BUILDTIME_DEPS && \
+ curl -fSsL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/postgresql.gpg > /dev/null && \
+ echo deb [arch=amd64,arm64,ppc64el signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ jammy-pgdg main | tee -a /etc/apt/sources.list.d/postgresql.list && \
+ apt-get update && \
+ apt-get install -y $RUNTIME_DEPS && \
+ apt-get -y autoremove && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*
-# Coolify requirements
-RUN apt-get install -y php8.2-pgsql openssh-client git git-lfs jq lsof
-RUN apt-get -y autoremove && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*
COPY --chmod=755 docker/dev/etc/s6-overlay/ /etc/s6-overlay/
COPY docker/dev/nginx.conf /etc/nginx/conf.d/custom.conf
-RUN echo "alias ll='ls -al'" >>/etc/bash.bashrc
-RUN echo "alias a='php artisan'" >>/etc/bash.bashrc
+RUN echo "alias ll='ls -al'" >>/etc/bash.bashrc && \
+ echo "alias a='php artisan'" >>/etc/bash.bashrc
RUN mkdir -p /usr/local/bin
-RUN /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/amd64' ]]; then \
+RUN --mount=type=cache,target=/root/.cache \
+ /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/amd64' ]]; then \
echo 'amd64' && \
curl -sSL https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \
;fi"
-RUN /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/arm64' ]]; then \
+RUN --mount=type=cache,target=/root/.cache \
+ /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/arm64' ]]; then \
echo 'arm64' && \
curl -L https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \
;fi"
diff --git a/docker/prod/Dockerfile b/docker/prod/Dockerfile
index d0cebcbca..37e0481bb 100644
--- a/docker/prod/Dockerfile
+++ b/docker/prod/Dockerfile
@@ -1,10 +1,10 @@
-FROM serversideup/php:8.2-fpm-nginx-v2.2.1 as base
+FROM serversideup/php:8.2-fpm-nginx-v2.2.1 AS base
WORKDIR /var/www/html
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-interaction --no-plugins --no-scripts --prefer-dist
-FROM node:20 as static-assets
+FROM node:20 AS static-assets
WORKDIR /app
COPY . .
COPY --from=base --chown=9999:9999 /var/www/html .
@@ -45,6 +45,8 @@ RUN composer dump-autoload
COPY --from=static-assets --chown=9999:9999 /app/public/build ./public/build
COPY --chmod=755 docker/prod/etc/s6-overlay/ /etc/s6-overlay/
+RUN php artisan route:clear
+RUN php artisan view:clear
RUN php artisan route:cache
RUN php artisan view:cache
diff --git a/lang/ar.json b/lang/ar.json
index c5ec96c8d..4b9afbe99 100644
--- a/lang/ar.json
+++ b/lang/ar.json
@@ -26,5 +26,12 @@
"input.code": "الرمز لمرة واحدة",
"input.recovery_code": "رمز الاسترداد",
"button.save": "حفظ",
- "repository.url": "أمثلة للمستودعات العامة، استخدم https://.... للمستودعات الخاصة، استخدم git@....
سيتم تحديد الفرع main لـ https://github.com/coollabsio/coolify-examples سيتم تحديد الفرع nodejs-fastify لـ https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify سيتم تحديد الفرع main لـ https://gitea.com/sedlav/expressjs.git سيتم تحديد الفرع main لـ https://gitlab.com/andrasbacsai/nodejs-example.git."
+ "repository.url": "أمثلة للمستودعات العامة، استخدم https://.... للمستودعات الخاصة، استخدم git@....
سيتم تحديد الفرع main لـ https://github.com/coollabsio/coolify-examples سيتم تحديد الفرع nodejs-fastify لـ https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify سيتم تحديد الفرع main لـ https://gitea.com/sedlav/expressjs.git سيتم تحديد الفرع main لـ https://gitlab.com/andrasbacsai/nodejs-example.git.",
+ "service.stop": "سيتم إيقاف هذه الخدمة.",
+ "resource.docker_cleanup": "قم بتشغيل Docker Cleanup (قم بإزالة الصور غير المستخدمة وذاكرة التخزين المؤقت للمنشئ).",
+ "resource.non_persistent": "سيتم حذف جميع البيانات غير الدائمة.",
+ "resource.delete_volumes": "حذف جميع المجلدات والملفات المرتبطة بهذا المورد بشكل دائم.",
+ "resource.delete_connected_networks": "حذف جميع الشبكات غير المحددة مسبقًا والمرتبطة بهذا المورد بشكل دائم.",
+ "resource.delete_configurations": "حذف جميع ملفات التعريف من الخادم بشكل دائم.",
+ "database.delete_backups_locally": "حذف كافة النسخ الاحتياطية نهائيًا من التخزين المحلي."
}
diff --git a/lang/en.json b/lang/en.json
index fa69c7035..5ea474b02 100644
--- a/lang/en.json
+++ b/lang/en.json
@@ -33,5 +33,6 @@
"resource.delete_volumes": "Permanently delete all volumes associated with this resource.",
"resource.delete_connected_networks": "Permanently delete all non-predefined networks associated with this resource.",
"resource.delete_configurations": "Permanently delete all configuration files from the server.",
- "database.delete_backups_locally": "All backups will be permanently deleted from local storage."
+ "database.delete_backups_locally": "All backups will be permanently deleted from local storage.",
+ "warning.sslipdomain": "Your configuration is saved, but sslip domain with https is NOT recommended, because Let's Encrypt servers with this public domain are rate limited (SSL certificate validation will fail).
Use your own domain instead."
}
diff --git a/lang/ro.json b/lang/ro.json
new file mode 100644
index 000000000..db1aa85db
--- /dev/null
+++ b/lang/ro.json
@@ -0,0 +1,37 @@
+{
+ "auth.login": "Autentificare",
+ "auth.login.azure": "Autentificare prin Microsoft",
+ "auth.login.bitbucket": "Autentificare prin Bitbucket",
+ "auth.login.github": "Autentificare prin GitHub",
+ "auth.login.gitlab": "Autentificare prin Gitlab",
+ "auth.login.google": "Autentificare prin Google",
+ "auth.already_registered": "Sunteți deja înregistrat?",
+ "auth.confirm_password": "Confirmați parola",
+ "auth.forgot_password": "Ați uitat parola",
+ "auth.forgot_password_send_email": "Trimiteți e-mail-ul pentru resetarea parolei",
+ "auth.register_now": "Înregistrare",
+ "auth.logout": "Deconectare",
+ "auth.register": "Înregistrare",
+ "auth.registration_disabled": "Înregistrarea este dezactivată. Vă rugăm să contactați administratorul site-ului.",
+ "auth.reset_password": "Resetare parolă",
+ "auth.failed": "Autentificare nereușită. Vă rugăm să verificați datele introduse.",
+ "auth.failed.callback": "A apărut o eroare în timpul autentificării cu furnizorul extern.",
+ "auth.failed.password": "Parola furnizată este incorectă.",
+ "auth.failed.email": "Nu putem găsi un utilizator cu această adresă de e-mail.",
+ "auth.throttle": "Prea multe încercări de autentificare. Vă rugăm să încercați din nou în :seconds secunde.",
+ "input.name": "Nume",
+ "input.email": "E-mail",
+ "input.password": "Parolă",
+ "input.password.again": "Repetați parola",
+ "input.code": "Cod de unică folosință",
+ "input.recovery_code": "Cod de recuperare",
+ "button.save": "Salvare",
+ "repository.url": "Exemple Pentru depozite publice, utilizați https://.... Pentru depozite private, utilizați git@....
https://github.com/coollabsio/coolify-examples va fi selectată ramura main https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify va fi selectată ramura nodejs-fastify. https://gitea.com/sedlav/expressjs.git va fi selectată ramura main. https://gitlab.com/andrasbacsai/nodejs-example.git va fi selectată ramura main.",
+ "service.stop": "Acest serviciu va fi oprit.",
+ "resource.docker_cleanup": "Executați curățarea Docker (eliminați imaginile neutilizate și memoria cache a constructorului).",
+ "resource.non_persistent": "Toate datele nepersistente vor fi șterse.",
+ "resource.delete_volumes": "Ștergeți definitiv toate volumele asociate cu această resursă.",
+ "resource.delete_connected_networks": "Ștergeți definitiv toate rețelele non-predefinite asociate cu această resursă.",
+ "resource.delete_configurations": "Ștergeți definitiv toate fișierele de configurare de pe server.",
+ "database.delete_backups_locally": "Toate copiile de rezervă vor fi șterse definitiv din stocarea locală."
+}
diff --git a/openapi.yaml b/openapi.yaml
index 91d5c1443..d2616e9c6 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -98,6 +98,10 @@ paths:
is_static:
type: boolean
description: 'The flag to indicate if the application is static.'
+ static_image:
+ type: string
+ enum: ['nginx:alpine']
+ description: 'The static image.'
install_command:
type: string
description: 'The install command.'
@@ -323,6 +327,10 @@ paths:
is_static:
type: boolean
description: 'The flag to indicate if the application is static.'
+ static_image:
+ type: string
+ enum: ['nginx:alpine']
+ description: 'The static image.'
install_command:
type: string
description: 'The install command.'
@@ -548,6 +556,10 @@ paths:
is_static:
type: boolean
description: 'The flag to indicate if the application is static.'
+ static_image:
+ type: string
+ enum: ['nginx:alpine']
+ description: 'The static image.'
install_command:
type: string
description: 'The install command.'
@@ -3093,7 +3105,7 @@ paths:
security:
-
bearerAuth: []
- /healthcheck:
+ /health:
get:
summary: Healthcheck
description: 'Healthcheck endpoint.'
@@ -4959,7 +4971,7 @@ components:
type: boolean
is_reachable:
type: boolean
- is_server_api_enabled:
+ is_sentinel_enabled:
type: boolean
is_swarm_manager:
type: boolean
@@ -4981,11 +4993,11 @@ components:
type: string
logdrain_newrelic_license_key:
type: string
- metrics_history_days:
+ sentinel_metrics_history_days:
type: integer
- metrics_refresh_rate_seconds:
+ sentinel_metrics_refresh_rate_seconds:
type: integer
- metrics_token:
+ sentinel_token:
type: string
docker_cleanup_frequency:
type: string
diff --git a/other/nightly/install.sh b/other/nightly/install.sh
index 04faf50ea..2371cca2c 100755
--- a/other/nightly/install.sh
+++ b/other/nightly/install.sh
@@ -13,7 +13,7 @@ DOCKER_VERSION="26.0"
# TODO: Ask for a user
CURRENT_USER=$USER
-mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,webhooks-during-maintenance,metrics,logs}
+mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,webhooks-during-maintenance,sentinel}
mkdir -p /data/coolify/ssh/{keys,mux}
mkdir -p /data/coolify/proxy/dynamic
@@ -164,7 +164,6 @@ sles | opensuse-leap | opensuse-tumbleweed)
esac
-
echo -e "2. Check OpenSSH server configuration. "
# Detect OpenSSH server
@@ -186,11 +185,51 @@ elif [ -x "$(command -v service)" ]; then
SSH_DETECTED=true
fi
fi
+
+
if [ "$SSH_DETECTED" = "false" ]; then
- echo "###############################################################################"
- echo "WARNING: Could not detect if OpenSSH server is installed and running - this does not mean that it is not installed, just that we could not detect it."
- echo -e "Please make sure it is set, otherwise Coolify cannot connect to the host system. \n"
- echo "###############################################################################"
+ echo " - OpenSSH server not detected. Installing OpenSSH server."
+ case "$OS_TYPE" in
+ arch)
+ pacman -Sy --noconfirm openssh >/dev/null
+ systemctl enable sshd >/dev/null 2>&1
+ systemctl start sshd >/dev/null 2>&1
+ ;;
+ alpine)
+ apk add openssh >/dev/null
+ rc-update add sshd default >/dev/null 2>&1
+ service sshd start >/dev/null 2>&1
+ ;;
+ ubuntu | debian | raspbian)
+ apt-get update -y >/dev/null
+ apt-get install -y openssh-server >/dev/null
+ systemctl enable ssh >/dev/null 2>&1
+ systemctl start ssh >/dev/null 2>&1
+ ;;
+ centos | fedora | rhel | ol | rocky | almalinux | amzn)
+ if [ "$OS_TYPE" = "amzn" ]; then
+ dnf install -y openssh-server >/dev/null
+ else
+ dnf install -y openssh-server >/dev/null
+ fi
+ systemctl enable sshd >/dev/null 2>&1
+ systemctl start sshd >/dev/null 2>&1
+ ;;
+ sles | opensuse-leap | opensuse-tumbleweed)
+ zypper install -y openssh >/dev/null
+ systemctl enable sshd >/dev/null 2>&1
+ systemctl start sshd >/dev/null 2>&1
+ ;;
+ *)
+ echo "###############################################################################"
+ echo "WARNING: Could not detect and install OpenSSH server - this does not mean that it is not installed or not running, just that we could not detect it."
+ echo -e "Please make sure it is installed and running, otherwise Coolify cannot connect to the host system. \n"
+ echo "###############################################################################"
+ exit 1
+ ;;
+ esac
+ echo " - OpenSSH server installed successfully."
+ SSH_DETECTED=true
fi
# Detect SSH PermitRootLogin
@@ -262,9 +301,14 @@ if ! [ -x "$(command -v docker)" ]; then
fi
;;
*)
- curl -s https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh >/dev/null 2>&1
+ if [ "$OS_TYPE" = "ubuntu" ] && [ "$OS_VERSION" = "24.10" ]; then
+ echo "Docker automated installation is not supported on Ubuntu 24.10 (non-LTS release)."
+ echo "Please install Docker manually."
+ exit 1
+ fi
+ curl -s https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh 2>&1
if ! [ -x "$(command -v docker)" ]; then
- curl -s https://get.docker.com | sh -s -- --version ${DOCKER_VERSION} >/dev/null 2>&1
+ curl -s https://get.docker.com | sh -s -- --version ${DOCKER_VERSION} 2>&1
if ! [ -x "$(command -v docker)" ]; then
echo " - Docker installation failed."
echo " Maybe your OS is not supported?"
@@ -287,7 +331,10 @@ test -s /etc/docker/daemon.json && cp /etc/docker/daemon.json /etc/docker/daemon
"log-opts": {
"max-size": "10m",
"max-file": "3"
- }
+ },
+ "default-address-pools": [
+ {"base":"10.0.0.0/8","size":24}
+ ]
}
EOL
cat >/etc/docker/daemon.json.coolify </etc/docker/daemon.json.coolify </dev/null 2>&1
+bash /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}"
echo " - Coolify installed successfully."
rm -f $ENV_FILE-$DATE
diff --git a/other/nightly/upgrade.sh b/other/nightly/upgrade.sh
index 9aa3a5f9a..4a9049a5e 100644
--- a/other/nightly/upgrade.sh
+++ b/other/nightly/upgrade.sh
@@ -1,10 +1,11 @@
#!/bin/bash
## Do not modify this file. You will lose the ability to autoupdate!
-VERSION="1.1"
+VERSION="1.2"
CDN="https://cdn.coollabs.io/coolify-nightly"
LATEST_IMAGE=${1:-latest}
LATEST_HELPER_VERSION=${2:-latest}
+DATE=$(date +%Y-%m-%d-%H-%M-%S)
curl -fsSL $CDN/docker-compose.yml -o /data/coolify/source/docker-compose.yml
curl -fsSL $CDN/docker-compose.prod.yml -o /data/coolify/source/docker-compose.prod.yml
@@ -32,7 +33,7 @@ docker network create --attachable coolify 2>/dev/null
if [ -f /data/coolify/source/docker-compose.custom.yml ]; then
echo "docker-compose.custom.yml detected."
- docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock --rm ghcr.io/coollabsio/coolify-helper:${LATEST_HELPER_VERSION:-latest} bash -c "LATEST_IMAGE=${1:-} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --force-recreate --wait --wait-timeout 60"
+ docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock --rm ghcr.io/coollabsio/coolify-helper:${LATEST_HELPER_VERSION:-latest} bash -c "LATEST_IMAGE=${1:-} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --force-recreate --wait --wait-timeout 60" > /data/coolify/source/upgrade-${DATE}.log 2>&1
else
- docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock --rm ghcr.io/coollabsio/coolify-helper:${LATEST_HELPER_VERSION:-latest} bash -c "LATEST_IMAGE=${1:-} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --force-recreate --wait --wait-timeout 60"
+ docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock --rm ghcr.io/coollabsio/coolify-helper:${LATEST_HELPER_VERSION:-latest} bash -c "LATEST_IMAGE=${1:-} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --force-recreate --wait --wait-timeout 60" > /data/coolify/source/upgrade-${DATE}.log 2>&1
fi
diff --git a/other/nightly/versions.json b/other/nightly/versions.json
index c04a3dee6..eeb9d77e9 100644
--- a/other/nightly/versions.json
+++ b/other/nightly/versions.json
@@ -1,16 +1,19 @@
{
"coolify": {
"v4": {
- "version": "4.0.0-beta.354"
+ "version": "4.0.0-beta.363"
},
"nightly": {
- "version": "4.0.0-beta.355"
+ "version": "4.0.0-beta.364"
},
"helper": {
- "version": "1.0.2"
+ "version": "1.0.3"
},
"realtime": {
- "version": "1.0.3"
+ "version": "1.0.4"
+ },
+ "sentinel": {
+ "version": "0.0.15"
}
}
}
diff --git a/public/svgs/affine.svg b/public/svgs/affine.svg
new file mode 100644
index 000000000..d8063e920
--- /dev/null
+++ b/public/svgs/affine.svg
@@ -0,0 +1,88 @@
+
diff --git a/public/svgs/calcom.svg b/public/svgs/calcom.svg
new file mode 100644
index 000000000..446b16655
--- /dev/null
+++ b/public/svgs/calcom.svg
@@ -0,0 +1,9 @@
+
\ No newline at end of file
diff --git a/public/svgs/cloudbeaver.svg b/public/svgs/cloudbeaver.svg
new file mode 100644
index 000000000..4a7634766
--- /dev/null
+++ b/public/svgs/cloudbeaver.svg
@@ -0,0 +1,7 @@
+
diff --git a/public/svgs/coder.svg b/public/svgs/coder.svg
new file mode 100644
index 000000000..45b7f795c
--- /dev/null
+++ b/public/svgs/coder.svg
@@ -0,0 +1,8 @@
+
diff --git a/public/svgs/cryptgeon.png b/public/svgs/cryptgeon.png
new file mode 100644
index 000000000..be121cfd0
Binary files /dev/null and b/public/svgs/cryptgeon.png differ
diff --git a/public/svgs/dify.png b/public/svgs/dify.png
new file mode 100644
index 000000000..326acf789
Binary files /dev/null and b/public/svgs/dify.png differ
diff --git a/public/svgs/edgedb.svg b/public/svgs/edgedb.svg
new file mode 100644
index 000000000..a906f7f7e
--- /dev/null
+++ b/public/svgs/edgedb.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/svgs/flowise.png b/public/svgs/flowise.png
new file mode 100644
index 000000000..6b0be0d2a
Binary files /dev/null and b/public/svgs/flowise.png differ
diff --git a/public/svgs/foundryvtt.png b/public/svgs/foundryvtt.png
new file mode 100644
index 000000000..c6a04508f
Binary files /dev/null and b/public/svgs/foundryvtt.png differ
diff --git a/public/svgs/freshrss.png b/public/svgs/freshrss.png
new file mode 100644
index 000000000..d1a75118f
Binary files /dev/null and b/public/svgs/freshrss.png differ
diff --git a/public/svgs/heyform.svg b/public/svgs/heyform.svg
new file mode 100644
index 000000000..ff29ca654
--- /dev/null
+++ b/public/svgs/heyform.svg
@@ -0,0 +1,5 @@
+
diff --git a/public/svgs/immich.svg b/public/svgs/immich.svg
new file mode 100644
index 000000000..9d844a772
--- /dev/null
+++ b/public/svgs/immich.svg
@@ -0,0 +1,66 @@
+
+
+
diff --git a/public/svgs/jenkins.svg b/public/svgs/jenkins.svg
new file mode 100644
index 000000000..0529fff1e
--- /dev/null
+++ b/public/svgs/jenkins.svg
@@ -0,0 +1,283 @@
+
+
+
+
\ No newline at end of file
diff --git a/public/svgs/jitsi.svg b/public/svgs/jitsi.svg
new file mode 100644
index 000000000..6257659ee
--- /dev/null
+++ b/public/svgs/jitsi.svg
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/public/svgs/kimai.svg b/public/svgs/kimai.svg
new file mode 100644
index 000000000..35b146972
--- /dev/null
+++ b/public/svgs/kimai.svg
@@ -0,0 +1,67 @@
+
\ No newline at end of file
diff --git a/public/svgs/libretranslate.svg b/public/svgs/libretranslate.svg
new file mode 100644
index 000000000..103d47d60
--- /dev/null
+++ b/public/svgs/libretranslate.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/svgs/litequeen.svg b/public/svgs/litequeen.svg
new file mode 100644
index 000000000..aa0b8e038
--- /dev/null
+++ b/public/svgs/litequeen.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/svgs/martin.png b/public/svgs/martin.png
new file mode 100644
index 000000000..d1a99e148
Binary files /dev/null and b/public/svgs/martin.png differ
diff --git a/public/svgs/mindsdb.svg b/public/svgs/mindsdb.svg
new file mode 100644
index 000000000..53799dd1c
--- /dev/null
+++ b/public/svgs/mindsdb.svg
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/svgs/mosquitto.png b/public/svgs/mosquitto.png
new file mode 100644
index 000000000..eb287a7cd
Binary files /dev/null and b/public/svgs/mosquitto.png differ
diff --git a/public/svgs/ntfy.svg b/public/svgs/ntfy.svg
new file mode 100644
index 000000000..9e5b5136f
--- /dev/null
+++ b/public/svgs/ntfy.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/svgs/osticket.png b/public/svgs/osticket.png
new file mode 100644
index 000000000..65885b71b
Binary files /dev/null and b/public/svgs/osticket.png differ
diff --git a/public/svgs/owncloud.svg b/public/svgs/owncloud.svg
new file mode 100644
index 000000000..83631e3f5
--- /dev/null
+++ b/public/svgs/owncloud.svg
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/svgs/peppermint.png b/public/svgs/peppermint.png
new file mode 100644
index 000000000..38db83de0
Binary files /dev/null and b/public/svgs/peppermint.png differ
diff --git a/public/svgs/qbittorrent.svg b/public/svgs/qbittorrent.svg
new file mode 100644
index 000000000..69d8cf62a
--- /dev/null
+++ b/public/svgs/qbittorrent.svg
@@ -0,0 +1,16 @@
+
+
+ qbittorrent-new-light
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/svgs/traccar.png b/public/svgs/traccar.png
new file mode 100644
index 000000000..c747aea05
Binary files /dev/null and b/public/svgs/traccar.png differ
diff --git a/public/svgs/transmission.svg b/public/svgs/transmission.svg
new file mode 100644
index 000000000..9a11f77f4
--- /dev/null
+++ b/public/svgs/transmission.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/svgs/unsend.svg b/public/svgs/unsend.svg
new file mode 100644
index 000000000..f5ff6fabc
--- /dev/null
+++ b/public/svgs/unsend.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/svgs/vvveb.svg b/public/svgs/vvveb.svg
new file mode 100644
index 000000000..2b66b3087
--- /dev/null
+++ b/public/svgs/vvveb.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/svgs/wireguard.svg b/public/svgs/wireguard.svg
new file mode 100644
index 000000000..81823b3eb
--- /dev/null
+++ b/public/svgs/wireguard.svg
@@ -0,0 +1,7 @@
+
+
\ No newline at end of file
diff --git a/public/svgs/zep.png b/public/svgs/zep.png
new file mode 100644
index 000000000..7d51b32dc
Binary files /dev/null and b/public/svgs/zep.png differ
diff --git a/public/svgs/zipline.png b/public/svgs/zipline.png
new file mode 100644
index 000000000..2b8f6972d
Binary files /dev/null and b/public/svgs/zipline.png differ
diff --git a/public/vendor/telescope/app.js b/public/vendor/telescope/app.js
index d1c0e7f28..378d6cf43 100644
--- a/public/vendor/telescope/app.js
+++ b/public/vendor/telescope/app.js
@@ -1,2 +1,2 @@
/*! For license information please see app.js.LICENSE.txt */
-(()=>{var t,e={2110:(t,e,n)=>{"use strict";var o=Object.freeze({}),p=Array.isArray;function M(t){return null==t}function b(t){return null!=t}function c(t){return!0===t}function r(t){return"string"==typeof t||"number"==typeof t||"symbol"==typeof t||"boolean"==typeof t}function z(t){return"function"==typeof t}function a(t){return null!==t&&"object"==typeof t}var i=Object.prototype.toString;function O(t){return"[object Object]"===i.call(t)}function s(t){return"[object RegExp]"===i.call(t)}function A(t){var e=parseFloat(String(t));return e>=0&&Math.floor(e)===e&&isFinite(t)}function u(t){return b(t)&&"function"==typeof t.then&&"function"==typeof t.catch}function l(t){return null==t?"":Array.isArray(t)||O(t)&&t.toString===i?JSON.stringify(t,null,2):String(t)}function d(t){var e=parseFloat(t);return isNaN(e)?t:e}function f(t,e){for(var n=Object.create(null),o=t.split(","),p=0;p-1)return t.splice(o,1)}}var v=Object.prototype.hasOwnProperty;function R(t,e){return v.call(t,e)}function m(t){var e=Object.create(null);return function(n){return e[n]||(e[n]=t(n))}}var g=/-(\w)/g,L=m((function(t){return t.replace(g,(function(t,e){return e?e.toUpperCase():""}))})),y=m((function(t){return t.charAt(0).toUpperCase()+t.slice(1)})),_=/\B([A-Z])/g,N=m((function(t){return t.replace(_,"-$1").toLowerCase()}));var E=Function.prototype.bind?function(t,e){return t.bind(e)}:function(t,e){function n(n){var o=arguments.length;return o?o>1?t.apply(e,arguments):t.call(e,n):t.call(e)}return n._length=t.length,n};function T(t,e){e=e||0;for(var n=t.length-e,o=new Array(n);n--;)o[n]=t[n+e];return o}function B(t,e){for(var n in e)t[n]=e[n];return t}function C(t){for(var e={},n=0;n0,tt=Z&&Z.indexOf("edge/")>0;Z&&Z.indexOf("android");var et=Z&&/iphone|ipad|ipod|ios/.test(Z);Z&&/chrome\/\d+/.test(Z),Z&&/phantomjs/.test(Z);var nt,ot=Z&&Z.match(/firefox\/(\d+)/),pt={}.watch,Mt=!1;if(K)try{var bt={};Object.defineProperty(bt,"passive",{get:function(){Mt=!0}}),window.addEventListener("test-passive",null,bt)}catch(t){}var ct=function(){return void 0===nt&&(nt=!K&&void 0!==n.g&&(n.g.process&&"server"===n.g.process.env.VUE_ENV)),nt},rt=K&&window.__VUE_DEVTOOLS_GLOBAL_HOOK__;function zt(t){return"function"==typeof t&&/native code/.test(t.toString())}var at,it="undefined"!=typeof Symbol&&zt(Symbol)&&"undefined"!=typeof Reflect&&zt(Reflect.ownKeys);at="undefined"!=typeof Set&&zt(Set)?Set:function(){function t(){this.set=Object.create(null)}return t.prototype.has=function(t){return!0===this.set[t]},t.prototype.add=function(t){this.set[t]=!0},t.prototype.clear=function(){this.set=Object.create(null)},t}();var Ot=null;function st(t){void 0===t&&(t=null),t||Ot&&Ot._scope.off(),Ot=t,t&&t._scope.on()}var At=function(){function t(t,e,n,o,p,M,b,c){this.tag=t,this.data=e,this.children=n,this.text=o,this.elm=p,this.ns=void 0,this.context=M,this.fnContext=void 0,this.fnOptions=void 0,this.fnScopeId=void 0,this.key=e&&e.key,this.componentOptions=b,this.componentInstance=void 0,this.parent=void 0,this.raw=!1,this.isStatic=!1,this.isRootInsert=!0,this.isComment=!1,this.isCloned=!1,this.isOnce=!1,this.asyncFactory=c,this.asyncMeta=void 0,this.isAsyncPlaceholder=!1}return Object.defineProperty(t.prototype,"child",{get:function(){return this.componentInstance},enumerable:!1,configurable:!0}),t}(),ut=function(t){void 0===t&&(t="");var e=new At;return e.text=t,e.isComment=!0,e};function lt(t){return new At(void 0,void 0,void 0,String(t))}function dt(t){var e=new At(t.tag,t.data,t.children&&t.children.slice(),t.text,t.elm,t.context,t.componentOptions,t.asyncFactory);return e.ns=t.ns,e.isStatic=t.isStatic,e.key=t.key,e.isComment=t.isComment,e.fnContext=t.fnContext,e.fnOptions=t.fnOptions,e.fnScopeId=t.fnScopeId,e.asyncMeta=t.asyncMeta,e.isCloned=!0,e}var ft=0,qt=[],ht=function(){for(var t=0;t0&&(Vt((o=Kt(o,"".concat(e||"","_").concat(n)))[0])&&Vt(a)&&(i[z]=lt(a.text+o[0].text),o.shift()),i.push.apply(i,o)):r(o)?Vt(a)?i[z]=lt(a.text+o):""!==o&&i.push(lt(o)):Vt(o)&&Vt(a)?i[z]=lt(a.text+o.text):(c(t._isVList)&&b(o.tag)&&M(o.key)&&b(e)&&(o.key="__vlist".concat(e,"_").concat(n,"__")),i.push(o)));return i}var Zt=1,Qt=2;function Jt(t,e,n,o,M,i){return(p(n)||r(n))&&(M=o,o=n,n=void 0),c(i)&&(M=Qt),function(t,e,n,o,M){if(b(n)&&b(n.__ob__))return ut();b(n)&&b(n.is)&&(e=n.is);if(!e)return ut();0;p(o)&&z(o[0])&&((n=n||{}).scopedSlots={default:o[0]},o.length=0);M===Qt?o=$t(o):M===Zt&&(o=function(t){for(var e=0;e0,c=e?!!e.$stable:!b,r=e&&e.$key;if(e){if(e._normalized)return e._normalized;if(c&&p&&p!==o&&r===p.$key&&!b&&!p.$hasNormal)return p;for(var z in M={},e)e[z]&&"$"!==z[0]&&(M[z]=he(t,n,z,e[z]))}else M={};for(var a in n)a in M||(M[a]=We(n,a));return e&&Object.isExtensible(e)&&(e._normalized=M),Y(M,"$stable",c),Y(M,"$key",r),Y(M,"$hasNormal",b),M}function he(t,e,n,o){var M=function(){var e=Ot;st(t);var n=arguments.length?o.apply(null,arguments):o({}),M=(n=n&&"object"==typeof n&&!p(n)?[n]:$t(n))&&n[0];return st(e),n&&(!M||1===n.length&&M.isComment&&!fe(M))?void 0:n};return o.proxy&&Object.defineProperty(e,n,{get:M,enumerable:!0,configurable:!0}),M}function We(t,e){return function(){return t[e]}}function ve(t){return{get attrs(){if(!t._attrsProxy){var e=t._attrsProxy={};Y(e,"_v_attr_proxy",!0),Re(e,t.$attrs,o,t,"$attrs")}return t._attrsProxy},get listeners(){t._listenersProxy||Re(t._listenersProxy={},t.$listeners,o,t,"$listeners");return t._listenersProxy},get slots(){return function(t){t._slotsProxy||ge(t._slotsProxy={},t.$scopedSlots);return t._slotsProxy}(t)},emit:E(t.$emit,t),expose:function(e){e&&Object.keys(e).forEach((function(n){return Ut(t,e,n)}))}}}function Re(t,e,n,o,p){var M=!1;for(var b in e)b in t?e[b]!==n[b]&&(M=!0):(M=!0,me(t,b,o,p));for(var b in t)b in e||(M=!0,delete t[b]);return M}function me(t,e,n,o){Object.defineProperty(t,e,{enumerable:!0,configurable:!0,get:function(){return n[o][e]}})}function ge(t,e){for(var n in e)t[n]=e[n];for(var n in t)n in e||delete t[n]}var Le,ye=null;function _e(t,e){return(t.__esModule||it&&"Module"===t[Symbol.toStringTag])&&(t=t.default),a(t)?e.extend(t):t}function Ne(t){if(p(t))for(var e=0;edocument.createEvent("Event").timeStamp&&(Ye=function(){return $e.now()})}var Ve=function(t,e){if(t.post){if(!e.post)return 1}else if(e.post)return-1;return t.id-e.id};function Ke(){var t,e;for(Ge=Ye(),Fe=!0,De.sort(Ve),He=0;HeHe&&De[n].id>t.id;)n--;De.splice(n+1,0,t)}else De.push(t);je||(je=!0,ln(Ke))}}var Qe="watcher";"".concat(Qe," callback"),"".concat(Qe," getter"),"".concat(Qe," cleanup");var Je;var tn=function(){function t(t){void 0===t&&(t=!1),this.detached=t,this.active=!0,this.effects=[],this.cleanups=[],this.parent=Je,!t&&Je&&(this.index=(Je.scopes||(Je.scopes=[])).push(this)-1)}return t.prototype.run=function(t){if(this.active){var e=Je;try{return Je=this,t()}finally{Je=e}}else 0},t.prototype.on=function(){Je=this},t.prototype.off=function(){Je=this.parent},t.prototype.stop=function(t){if(this.active){var e=void 0,n=void 0;for(e=0,n=this.effects.length;e-1)if(M&&!R(p,"default"))b=!1;else if(""===b||b===N(t)){var r=eo(String,p.type);(r<0||c-1:"string"==typeof t?t.split(",").indexOf(e)>-1:!!s(t)&&t.test(e)}function bo(t,e){var n=t.cache,o=t.keys,p=t._vnode;for(var M in n){var b=n[M];if(b){var c=b.name;c&&!e(c)&&co(n,M,o,p)}}}function co(t,e,n,o){var p=t[e];!p||o&&p.tag===o.tag||p.componentInstance.$destroy(),t[e]=null,W(n,e)}!function(t){t.prototype._init=function(t){var e=this;e._uid=Bn++,e._isVue=!0,e.__v_skip=!0,e._scope=new tn(!0),e._scope._vm=!0,t&&t._isComponent?function(t,e){var n=t.$options=Object.create(t.constructor.options),o=e._parentVnode;n.parent=e.parent,n._parentVnode=o;var p=o.componentOptions;n.propsData=p.propsData,n._parentListeners=p.listeners,n._renderChildren=p.children,n._componentTag=p.tag,e.render&&(n.render=e.render,n.staticRenderFns=e.staticRenderFns)}(e,t):e.$options=Vn(Cn(e.constructor),t||{},e),e._renderProxy=e,e._self=e,function(t){var e=t.$options,n=e.parent;if(n&&!e.abstract){for(;n.$options.abstract&&n.$parent;)n=n.$parent;n.$children.push(t)}t.$parent=n,t.$root=n?n.$root:t,t.$children=[],t.$refs={},t._provided=n?n._provided:Object.create(null),t._watcher=null,t._inactive=null,t._directInactive=!1,t._isMounted=!1,t._isDestroyed=!1,t._isBeingDestroyed=!1}(e),function(t){t._events=Object.create(null),t._hasHookEvent=!1;var e=t.$options._parentListeners;e&&Ce(t,e)}(e),function(t){t._vnode=null,t._staticTrees=null;var e=t.$options,n=t.$vnode=e._parentVnode,p=n&&n.context;t.$slots=le(e._renderChildren,p),t.$scopedSlots=n?qe(t.$parent,n.data.scopedSlots,t.$slots):o,t._c=function(e,n,o,p){return Jt(t,e,n,o,p,!1)},t.$createElement=function(e,n,o,p){return Jt(t,e,n,o,p,!0)};var M=n&&n.data;wt(t,"$attrs",M&&M.attrs||o,null,!0),wt(t,"$listeners",e._parentListeners||o,null,!0)}(e),Ie(e,"beforeCreate",void 0,!1),function(t){var e=Tn(t.$options.inject,t);e&&(Et(!1),Object.keys(e).forEach((function(n){wt(t,n,e[n])})),Et(!0))}(e),gn(e),function(t){var e=t.$options.provide;if(e){var n=z(e)?e.call(t):e;if(!a(n))return;for(var o=en(t),p=it?Reflect.ownKeys(n):Object.keys(n),M=0;M1?T(n):n;for(var o=T(arguments,1),p='event handler for "'.concat(t,'"'),M=0,b=n.length;MparseInt(this.max)&&co(e,n[0],n,this._vnode),this.vnodeToCache=null}}},created:function(){this.cache=Object.create(null),this.keys=[]},destroyed:function(){for(var t in this.cache)co(this.cache,t,this.keys)},mounted:function(){var t=this;this.cacheVNode(),this.$watch("include",(function(e){bo(t,(function(t){return Mo(e,t)}))})),this.$watch("exclude",(function(e){bo(t,(function(t){return!Mo(e,t)}))}))},updated:function(){this.cacheVNode()},render:function(){var t=this.$slots.default,e=Ne(t),n=e&&e.componentOptions;if(n){var o=po(n),p=this.include,M=this.exclude;if(p&&(!o||!Mo(p,o))||M&&o&&Mo(M,o))return e;var b=this.cache,c=this.keys,r=null==e.key?n.Ctor.cid+(n.tag?"::".concat(n.tag):""):e.key;b[r]?(e.componentInstance=b[r].componentInstance,W(c,r),c.push(r)):(this.vnodeToCache=e,this.keyToCache=r),e.data.keepAlive=!0}return e||t&&t[0]}},ao={KeepAlive:zo};!function(t){var e={get:function(){return F}};Object.defineProperty(t,"config",e),t.util={warn:Un,extend:B,mergeOptions:Vn,defineReactive:wt},t.set=St,t.delete=Xt,t.nextTick=ln,t.observable=function(t){return Ct(t),t},t.options=Object.create(null),U.forEach((function(e){t.options[e+"s"]=Object.create(null)})),t.options._base=t,B(t.options.components,ao),function(t){t.use=function(t){var e=this._installedPlugins||(this._installedPlugins=[]);if(e.indexOf(t)>-1)return this;var n=T(arguments,1);return n.unshift(this),z(t.install)?t.install.apply(t,n):z(t)&&t.apply(null,n),e.push(t),this}}(t),function(t){t.mixin=function(t){return this.options=Vn(this.options,t),this}}(t),oo(t),function(t){U.forEach((function(e){t[e]=function(t,n){return n?("component"===e&&O(n)&&(n.name=n.name||t,n=this.options._base.extend(n)),"directive"===e&&z(n)&&(n={bind:n,update:n}),this.options[e+"s"][t]=n,n):this.options[e+"s"][t]}}))}(t)}(no),Object.defineProperty(no.prototype,"$isServer",{get:ct}),Object.defineProperty(no.prototype,"$ssrContext",{get:function(){return this.$vnode&&this.$vnode.ssrContext}}),Object.defineProperty(no,"FunctionalRenderContext",{value:wn}),no.version="2.7.14";var io=f("style,class"),Oo=f("input,textarea,option,select,progress"),so=function(t,e,n){return"value"===n&&Oo(t)&&"button"!==e||"selected"===n&&"option"===t||"checked"===n&&"input"===t||"muted"===n&&"video"===t},Ao=f("contenteditable,draggable,spellcheck"),uo=f("events,caret,typing,plaintext-only"),lo=function(t,e){return vo(e)||"false"===e?"false":"contenteditable"===t&&uo(e)?e:"true"},fo=f("allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,default,defaultchecked,defaultmuted,defaultselected,defer,disabled,enabled,formnovalidate,hidden,indeterminate,inert,ismap,itemscope,loop,multiple,muted,nohref,noresize,noshade,novalidate,nowrap,open,pauseonexit,readonly,required,reversed,scoped,seamless,selected,sortable,truespeed,typemustmatch,visible"),qo="http://www.w3.org/1999/xlink",ho=function(t){return":"===t.charAt(5)&&"xlink"===t.slice(0,5)},Wo=function(t){return ho(t)?t.slice(6,t.length):""},vo=function(t){return null==t||!1===t};function Ro(t){for(var e=t.data,n=t,o=t;b(o.componentInstance);)(o=o.componentInstance._vnode)&&o.data&&(e=mo(o.data,e));for(;b(n=n.parent);)n&&n.data&&(e=mo(e,n.data));return function(t,e){if(b(t)||b(e))return go(t,Lo(e));return""}(e.staticClass,e.class)}function mo(t,e){return{staticClass:go(t.staticClass,e.staticClass),class:b(t.class)?[t.class,e.class]:e.class}}function go(t,e){return t?e?t+" "+e:t:e||""}function Lo(t){return Array.isArray(t)?function(t){for(var e,n="",o=0,p=t.length;o