Compare commits

...

32 Commits

Author SHA1 Message Date
Andras Bacsai
a4cc406114 Merge pull request #1212 from coollabsio/next
v4.0.0-beta.29
2023-09-08 18:52:41 +02:00
Andras Bacsai
53b15a5762 update sentry dsn 2023-09-08 18:40:32 +02:00
Andras Bacsai
929a4e6474 fix: coolify already exists should not throw error 2023-09-08 18:40:25 +02:00
Andras Bacsai
45b597bbab feat: cache team settings 2023-09-08 18:33:26 +02:00
Andras Bacsai
0d1a2aa5d1 update testemail command 2023-09-08 17:51:19 +02:00
Andras Bacsai
b82353d5e2 update testemail command 2023-09-08 17:42:08 +02:00
Andras Bacsai
b17c09f7a7 update testemail command 2023-09-08 17:31:02 +02:00
Andras Bacsai
f6c3fe7888 fix: test email on for admins or custom smtp 2023-09-08 17:26:59 +02:00
Andras Bacsai
2e855e030f fix: ui 2023-09-08 16:59:49 +02:00
Andras Bacsai
49f86621f4 fix: instance email settings 2023-09-08 16:56:14 +02:00
Andras Bacsai
03d9f93397 fix: retry notifications 2023-09-08 16:53:19 +02:00
Andras Bacsai
c472042a94 fix: ui 2023-09-08 16:46:53 +02:00
Andras Bacsai
9f4356f67d version++ 2023-09-08 16:28:56 +02:00
Andras Bacsai
a50317cc76 Merge pull request #1209 from coollabsio/next
v4.0.0-beta.28
2023-09-08 16:28:04 +02:00
Andras Bacsai
8afa98a1ca fix: sentry 4451028626 2023-09-08 16:19:45 +02:00
Andras Bacsai
f6737f21dd feat: developer view for env variables 2023-09-08 16:16:59 +02:00
Andras Bacsai
e4a51cc116 fix: sentry 4459819517 2023-09-08 16:16:28 +02:00
Andras Bacsai
acd78ae196 feat: Telegram topics separation 2023-09-08 14:15:28 +02:00
Andras Bacsai
953bcfb5bb fix: db backup job 2023-09-08 12:23:46 +02:00
Andras Bacsai
dacfab8b29 Update BUG_REPORT.yml 2023-09-08 12:08:16 +02:00
Andras Bacsai
48b3e99939 Merge pull request #1203 from coollabsio/next
v4.0.0-beta.27
2023-09-08 12:06:34 +02:00
Andras Bacsai
41ad67c7c9 updates 2023-09-08 12:05:15 +02:00
Andras Bacsai
b49725cb1c fix: bug 2023-09-08 09:37:58 +02:00
Andras Bacsai
75e674a966 version++ 2023-09-08 09:27:44 +02:00
Andras Bacsai
9d826d9fb4 Merge pull request #1202 from coollabsio/next
v4.0.0-beta.26
2023-09-08 09:08:19 +02:00
Andras Bacsai
0af221f9fc udpate 2023-09-08 09:06:00 +02:00
Andras Bacsai
9066c9bf90 update readme 2023-09-08 09:00:44 +02:00
Andras Bacsai
77e3208f00 feat: public database 2023-09-07 13:23:34 +02:00
Andras Bacsai
db5ecf07bd version++ 2023-09-07 09:14:27 +02:00
Andras Bacsai
7f28aa6985 Merge pull request #1201 from coollabsio/next
v4.0.0-beta.25
2023-09-07 09:04:33 +02:00
Andras Bacsai
9d53e04ce9 fix: saveModel email settings 2023-09-07 08:48:16 +02:00
Andras Bacsai
3db9a1dd6e version++ 2023-09-06 15:26:17 +02:00
47 changed files with 553 additions and 142 deletions

26
.github/ISSUE_TEMPLATE/BUG_REPORT.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: Bug report
description: Create a new bug report
title: '[Bug]: '
body:
- type: textarea
attributes:
label: Description
description: A clear and concise description of the problem
validations:
required: true
- type: textarea
attributes:
label: Minimal Reproduction (if possible, example repository)
description: Please provide a step by step guide to reproduce the issue
validations:
required: true
- type: textarea
attributes:
label: Exception or Error
description: Please provide error logs if possible.
- type: input
attributes:
label: Version
description: Coolify's version (see bottom left corner).
validations:
required: true

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: 🤔 Community Support (Chat)
url: https://coollabs.io/discord
about: Reach out to us on Discord.
- name: 🙋‍♂️ Feature Requests
url: https://github.com/coollabsio/coolify/discussions/categories/feature-requests-ideas
about: All feature requests will be discussed here.

1
.gitignore vendored
View File

@@ -31,3 +31,4 @@ _ide_helper.php
_ide_helper_models.php
.rnd
/.ssh
scripts/load-test/*

View File

@@ -1,20 +1,30 @@
# Coolify v4 Beta
# About the Project
An open-source & self-hostable Heroku / Netlify alternative.
Coolify is an open-source & self-hostable alternative to Heroku / Netlify / Vercel / etc.
It helps you to manage your servers, applications, databases on your own hardware, all you need is SSH connection. You can manage VPS, Bare Metal, Raspberry PI's anything.
Image if you could have the ease of a cloud but with your own servers. That is **Coolify**.
No vendor lock-in, which means that all the configuration for your applications/databases/etc are saved to your server. So if you decide to stop using Coolify (oh nooo), you could still manage your running resources. You just lose the automations and all the magic. 🪄️
For more information, take a look at our landing page [here](https://coolify.io).
> If you are looking for previous (v3) version, it is [here](https://github.com/coollabsio/coolify/tree/v3).
# Cloud
If you do not want to self-host Coolify, there is a paid cloud version available: https://app.coolify.io
You can easily attach your own servers, get all the automations, free email notifications, etc.
For more information & pricing, take a look at our landing page [here](https://coolify.io).
# Beta
You are checking the next-gen of Coolify, aka v4. Hi 👋
The latest version (v4) is still in beta. That does not mean it is unstable. All the features that are available are stable enough be usable in real-life.
It is still in beta, lots of improvements will come every day. Things could break, but we are working hard to make it stable as soon as possible. If you find any bugs, please report them.
Automatic updates are available, so you will receive the latest version as soon as it is released.
If you are looking for v3, check out the [v3 branch](https://github.com/coollabsio/coolify/tree/v3).
## What's new?
Well, the whole tech stack changed, core is different, so yeah, a lot (documentation incoming).
There are hundreds of people using it for managing their client's applications, freelancers, hobbyists, businesses.
# Installation
@@ -26,13 +36,19 @@ You can find the installation script [here](./scripts/install.sh).
## Support
- Twitter: [@heyandras](https://twitter.com/heyandras)
- Mastodon: [@andrasbacsai@fosstodon.org](https://fosstodon.org/@andrasbacsai)
- Email: [andras@coollabs.io](mailto:andras@coollabs.io)
- Discord: [Invitation](https://coollabs.io/discord)
- Telegram: [@andrasbacsai](https://t.me/andrasbacsai)
Contact us [here](https://docs.coollabs.io/contact).
---
## Recognitions
<a href="https://news.ycombinator.com/item?id=26624341">
<img
style="width: 250px; height: 54px;" width="250" height="54"
alt="Featured on Hacker News"
src="https://hackernews-badge.vercel.app/api?id=26624341"
/>
</a>
<a href="https://www.producthunt.com/posts/coolify?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-coolify" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=338273&theme=light" alt="Coolify - An&#0032;open&#0045;source&#0032;&#0038;&#0032;self&#0045;hostable&#0032;Heroku&#0044;&#0032;Netlify&#0032;alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
## 💰 Financial Contributors

View File

@@ -40,7 +40,7 @@ class InstallDocker
"echo ####### Restarting Docker Engine...",
"systemctl restart docker",
"echo ####### Creating default network...",
"docker network create --attachable coolify",
"docker network create --attachable coolify >/dev/null 2>&1 || true",
"echo ####### Done!"
], $server);
$found = StandaloneDocker::where('server_id', $server->id);

View File

@@ -23,6 +23,7 @@ use Mail;
use Str;
use function Laravel\Prompts\select;
use function Laravel\Prompts\text;
class TestEmail extends Command
{
@@ -44,9 +45,10 @@ class TestEmail extends Command
* Execute the console command.
*/
private ?MailMessage $mail = null;
private string $email = 'andras.bacsai@protonmail.com';
public function handle()
{
$email = select(
$type = select(
'Which Email should be sent?',
options: [
'emails-test' => 'Test',
@@ -60,15 +62,15 @@ class TestEmail extends Command
'waitlist-confirmation' => 'Waitlist Confirmation',
],
);
$type = set_transanctional_email_settings();
if (!$type) {
throw new Exception('No email settings found.');
}
$this->email = text('Email Address to send to');
set_transanctional_email_settings();
$this->mail = new MailMessage();
$this->mail->subject("Test Email");
switch ($email) {
switch ($type) {
case 'emails-test':
$this->mail = (new Test())->toMail();
$this->sendEmail();
break;
case 'application-deployment-success':
$application = Application::all()->first();
@@ -172,11 +174,7 @@ class TestEmail extends Command
[],
[],
fn (Message $message) => $message
->from(
'internal@example.com',
'Test Email',
)
->to('test@example.com')
->to($this->email)
->subject($this->mail->subject)
->html((string)$this->mail->render())
);

View File

@@ -6,6 +6,7 @@ use App\Actions\Server\InstallDocker;
use App\Models\PrivateKey;
use App\Models\Project;
use App\Models\Server;
use App\Models\Team;
use Illuminate\Support\Collection;
use Livewire\Component;
@@ -70,9 +71,10 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
}
public function skipBoarding()
{
currentTeam()->update([
Team::find(currentTeam()->id)->update([
'show_boarding' => false
]);
ray(currentTeam());
refreshSession();
return redirect()->route('dashboard');
}

View File

@@ -107,7 +107,14 @@ class EmailSettings extends Component
return general_error_handler($e, $this);
}
}
public function saveModel()
{
$this->team->save();
if (is_a($this->team, Team::class)) {
refreshSession();
}
$this->emit('success', 'Settings saved.');
}
public function submit()
{
try {

View File

@@ -17,6 +17,10 @@ class TelegramSettings extends Component
'team.telegram_notifications_deployments' => 'nullable|boolean',
'team.telegram_notifications_status_changes' => 'nullable|boolean',
'team.telegram_notifications_database_backups' => 'nullable|boolean',
'team.telegram_notifications_test_message_thread_id' => 'nullable|string',
'team.telegram_notifications_deployments_message_thread_id' => 'nullable|string',
'team.telegram_notifications_status_changes_message_thread_id' => 'nullable|string',
'team.telegram_notifications_database_backups_message_thread_id' => 'nullable|string',
];
protected $validationAttributes = [
'team.telegram_token' => 'Token',

View File

@@ -42,8 +42,13 @@ class Heading extends Component
["docker rm -f {$this->database->uuid}"],
$this->database->destination->server
);
if ($this->database->is_public) {
stopPostgresProxy($this->database);
$this->database->is_public = false;
}
$this->database->status = 'stopped';
$this->database->save();
$this->emit('refresh');
// $this->database->environment->project->team->notify(new StatusChanged($this->database));
}

View File

@@ -12,6 +12,7 @@ class General extends Component
public StandalonePostgresql $database;
public string $new_filename;
public string $new_content;
public string $db_url;
protected $listeners = ['refresh', 'save_init_script', 'delete_init_script'];
@@ -26,6 +27,8 @@ class General extends Component
'database.init_scripts' => 'nullable',
'database.image' => 'required',
'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
];
protected $validationAttributes = [
'database.name' => 'Name',
@@ -38,8 +41,44 @@ class General extends Component
'database.init_scripts' => 'Init Scripts',
'database.image' => 'Image',
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
];
public function mount()
{
$this->getDbUrl();
}
public function getDbUrl() {
if ($this->database->is_public) {
$this->db_url = "postgres://{$this->database->postgres_user}:{$this->database->postgres_password}@{$this->database->destination->server->ip}:{$this->database->public_port}/{$this->database->postgres_db}";
} else {
$this->db_url = "postgres://{$this->database->postgres_user}:{$this->database->postgres_password}@{$this->database->uuid}:5432/{$this->database->postgres_db}";
}
}
public function instantSave()
{
try {
if ($this->database->is_public && !$this->database->public_port) {
$this->emit('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
$this->emit('success', 'Starting TCP proxy...');
startPostgresProxy($this->database);
$this->emit('success', 'Database is now publicly accessible.');
} else {
stopPostgresProxy($this->database);
$this->emit('success', 'Database is no longer publicly accessible.');
}
$this->getDbUrl();
$this->database->save();
} catch(Exception $e) {
$this->database->is_public = !$this->database->is_public;
return general_error_handler(err: $e, that: $this);
}
}
public function save_init_script($script)
{
$this->database->init_scripts = filter($this->database->init_scripts, fn ($s) => $s['filename'] !== $script['filename']);

View File

@@ -5,21 +5,84 @@ namespace App\Http\Livewire\Project\Shared\EnvironmentVariable;
use App\Models\EnvironmentVariable;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
use Str;
class All extends Component
{
public $resource;
public bool $showPreview = false;
public string|null $modalId = null;
public ?string $variables = null;
public ?string $variablesPreview = null;
public string $view = 'normal';
protected $listeners = ['refreshEnvs', 'submit'];
public function mount()
{
$resourceClass = get_class($this->resource);
$resourceWithPreviews = ['App\Models\Application'];
$simpleDockerfile = !is_null(data_get($this->resource, 'dockerfile'));
if (Str::of($resourceClass)->contains($resourceWithPreviews) && !$simpleDockerfile) {
$this->showPreview = true;
}
$this->modalId = new Cuid2(7);
$this->getDevView();
}
public function getDevView()
{
$this->variables = $this->resource->environment_variables->map(function ($item) {
return "$item->key=$item->value";
})->sort()->join('
');
if ($this->showPreview) {
$this->variablesPreview = $this->resource->environment_variables_preview->map(function ($item) {
return "$item->key=$item->value";
})->sort()->join('
');
}
}
public function switch()
{
$this->view = $this->view === 'normal' ? 'dev' : 'normal';
}
public function saveVariables($isPreview)
{
if ($isPreview) {
$variables = parseEnvFormatToArray($this->variablesPreview);
$existingVariables = $this->resource->environment_variables_preview();
$this->resource->environment_variables_preview()->delete();
} else {
$variables = parseEnvFormatToArray($this->variables);
$existingVariables = $this->resource->environment_variables();
$this->resource->environment_variables()->delete();
}
foreach ($variables as $key => $variable) {
$found = $existingVariables->where('key', $key)->first();
if ($found) {
$found->value = $variable;
$found->save();
continue;
} else {
$environment = new EnvironmentVariable();
$environment->key = $key;
$environment->value = $variable;
$environment->is_build_time = false;
$environment->is_preview = $isPreview ? true : false;
if ($this->resource->type() === 'application') {
$environment->application_id = $this->resource->id;
}
if ($this->resource->type() === 'standalone-postgresql') {
$environment->standalone_postgresql_id = $this->resource->id;
}
$environment->save();
}
}
$this->refreshEnvs();
}
public function refreshEnvs()
{
$this->resource->refresh();
$this->getDevView();
}
public function submit($data)
@@ -43,7 +106,7 @@ class All extends Component
$environment->standalone_postgresql_id = $this->resource->id;
}
$environment->save();
$this->resource->refresh();
$this->refreshEnvs();
$this->emit('success', 'Environment variable added successfully.');
} catch (\Exception $e) {
return general_error_handler(err: $e, that: $this);

View File

@@ -60,7 +60,6 @@ class Email extends Component
$this->validate([
'settings.resend_api_key' => 'required'
]);
$this->settings->smtp_enabled = false;
$this->settings->save();
$this->emit('success', 'Settings saved successfully.');
} catch (\Exception $e) {
@@ -68,9 +67,18 @@ class Email extends Component
return general_error_handler($e, $this);
}
}
public function instantSaveResend() {
try {
$this->settings->smtp_enabled = false;
$this->submitResend();
} catch (\Exception $e) {
return general_error_handler($e, $this);
}
}
public function instantSave()
{
try {
$this->settings->resend_enabled = false;
$this->submit();
} catch (\Exception $e) {
return general_error_handler($e, $this);
@@ -89,7 +97,6 @@ class Email extends Component
'settings.smtp_password' => 'nullable',
'settings.smtp_timeout' => 'nullable',
]);
$this->settings->resend_enabled = false;
$this->settings->save();
$this->emit('success', 'Settings saved successfully.');
} catch (\Exception $e) {

View File

@@ -23,7 +23,7 @@ class SwitchTeam extends Component
if (!$team_to_switch_to) {
return;
}
session(['currentTeam' => $team_to_switch_to]);
refreshSession($team_to_switch_to);
return redirect(request()->header('Referer'));
}
}

View File

@@ -24,28 +24,32 @@ class DatabaseBackupJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public Team|null $team = null;
public ?Team $team = null;
public Server $server;
public ScheduledDatabaseBackup|null $backup;
public ?ScheduledDatabaseBackup $backup = null;
public string $database_type;
public StandalonePostgresql $database;
public ?StandalonePostgresql $database = null;
public string $database_status;
public string|null $container_name = null;
public ScheduledDatabaseBackupExecution|null $backup_log = null;
public ?string $container_name = null;
public ?ScheduledDatabaseBackupExecution $backup_log = null;
public string $backup_status;
public string|null $backup_location = null;
public ?string $backup_location = null;
public string $backup_dir;
public string $backup_file;
public int $size = 0;
public string|null $backup_output = null;
public S3Storage|null $s3 = null;
public ?string $backup_output = null;
public ?S3Storage $s3 = null;
public function __construct($backup)
{
$this->backup = $backup;
$this->team = Team::find($backup->team_id);
$this->database = $this->backup->database;
if (!$this->database) {
ray('Database not found');
return;
}
$this->database_type = $this->database->type();
$this->server = $this->database->destination->server;
$this->database_status = $this->database->status;

View File

@@ -31,6 +31,7 @@ class SendMessageToTelegramJob implements ShouldQueue
public array $buttons,
public string $token,
public string $chatId,
public ?string $topicId = null,
) {
}
@@ -63,7 +64,9 @@ class SendMessageToTelegramJob implements ShouldQueue
'chat_id' => $this->chatId,
'text' => $this->text,
];
ray($payload);
if ($this->topicId) {
$payload['message_thread_id'] = $this->topicId;
}
$response = Http::post($url, $payload);
if ($response->failed()) {
throw new \Exception('Telegram notification failed with ' . $response->status() . ' status code.' . $response->body());

View File

@@ -105,7 +105,7 @@ class Application extends BaseModel
public function environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', false);
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', false)->orderBy('key', 'asc');
}
public function runtime_environment_variables(): HasMany
@@ -127,7 +127,7 @@ class Application extends BaseModel
public function environment_variables_preview(): HasMany
{
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true);
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->orderBy('key', 'asc');
}
public function runtime_environment_variables_preview(): HasMany

View File

@@ -20,13 +20,16 @@ class EnvironmentVariable extends Model
{
static::created(function ($environment_variable) {
if ($environment_variable->application_id && !$environment_variable->is_preview) {
ModelsEnvironmentVariable::create([
'key' => $environment_variable->key,
'value' => $environment_variable->value,
'is_build_time' => $environment_variable->is_build_time,
'application_id' => $environment_variable->application_id,
'is_preview' => true,
]);
$found = ModelsEnvironmentVariable::where('key', $environment_variable->key)->where('application_id', $environment_variable->application_id)->where('is_preview',true)->first();
if (!$found) {
ModelsEnvironmentVariable::create([
'key' => $environment_variable->key,
'value' => $environment_variable->value,
'is_build_time' => $environment_variable->is_build_time,
'application_id' => $environment_variable->application_id,
'is_preview' => true,
]);
}
}
});
}

View File

@@ -23,6 +23,6 @@ class StandaloneDocker extends BaseModel
public function attachedTo()
{
return $this->applications->count() > 0 || $this->databases->count() > 0;
return $this->applications?->count() > 0 || $this->databases?->count() > 0;
}
}

View File

@@ -28,7 +28,7 @@ class Team extends Model implements SendsDiscord, SendsEmail
{
return [
"token" => data_get($this, 'telegram_token', null),
"chat_id" => data_get($this, 'telegram_chat_id', null)
"chat_id" => data_get($this, 'telegram_chat_id', null),
];
}

View File

@@ -4,6 +4,7 @@ namespace App\Models;
use App\Notifications\Channels\SendsEmail;
use App\Notifications\TransactionalEmails\ResetPassword as TransactionalEmailsResetPassword;
use Cache;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
@@ -94,7 +95,9 @@ class User extends Authenticatable implements SendsEmail
public function currentTeam()
{
return Team::find(session('currentTeam')->id);
return Cache::remember('team:' . auth()->user()->id, 3600, function() {
return Team::find(session('currentTeam')->id);
});
}
public function otherTeams()

View File

@@ -16,6 +16,7 @@ class DeploymentFailed extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 5;
public Application $application;
public string $deployment_uuid;
public ?ApplicationPreview $preview = null;

View File

@@ -16,6 +16,7 @@ class DeploymentSuccess extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 5;
public Application $application;
public string $deployment_uuid;
public ApplicationPreview|null $preview = null;

View File

@@ -14,6 +14,7 @@ class StatusChanged extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 5;
public $application;
public string $application_name;

View File

@@ -10,16 +10,30 @@ class TelegramChannel
{
$data = $notification->toTelegram($notifiable);
$telegramData = $notifiable->routeNotificationForTelegram();
$message = data_get($data, 'message');
$buttons = data_get($data, 'buttons', []);
ray($message, $buttons);
$telegramToken = data_get($telegramData, 'token');
$chatId = data_get($telegramData, 'chat_id');
$topicId = null;
$topicsInstance = get_class($notification);
if (!$telegramToken || !$chatId || !$message) {
throw new \Exception('Telegram token, chat id and message are required');
switch ($topicsInstance) {
case 'App\Notifications\StatusChange':
$topicId = data_get($notifiable, 'telegram_notifications_status_changes_message_thread_id');
break;
case 'App\Notifications\Test':
$topicId = data_get($notifiable, 'telegram_notifications_test_message_thread_id');
break;
case 'App\Notifications\Deployment':
$topicId = data_get($notifiable, 'telegram_notifications_deployments_message_thread_id');
break;
case 'App\Notifications\DatabaseBackup':
$topicId = data_get($notifiable, 'telegram_notifications_database_backups_message_thread_id');
break;
}
dispatch(new SendMessageToTelegramJob($message, $buttons, $telegramToken, $chatId));
if (!$telegramToken || !$chatId || !$message) {
return;
}
dispatch(new SendMessageToTelegramJob($message, $buttons, $telegramToken, $chatId, $topicId));
}
}

View File

@@ -14,6 +14,7 @@ class BackupFailed extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 5;
public string $name;
public string $frequency;

View File

@@ -14,6 +14,7 @@ class BackupSuccess extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 5;
public string $name;
public string $frequency;

View File

@@ -12,6 +12,7 @@ class GeneralNotification extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 5;
public function __construct(public string $message)
{
}

View File

@@ -15,6 +15,7 @@ class NotReachable extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 5;
public function __construct(public Server $server)
{

View File

@@ -14,6 +14,7 @@ class Test extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 5;
public function __construct(public string|null $emails = null)
{
}
@@ -45,7 +46,7 @@ class Test extends Notification implements ShouldQueue
"buttons" => [
[
"text" => "Go to your dashboard",
"url" => 'https://coolify.io'
"url" => base_url()
]
],
];

View File

@@ -15,6 +15,7 @@ class InvitationLink extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 5;
public function via(): array
{
return [TransactionalEmailChannel::class];

View File

@@ -12,6 +12,7 @@ class Test extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 5;
public function __construct(public string $emails)
{
}

View File

@@ -20,7 +20,7 @@ function create_standalone_postgresql($environment_id, $destination_uuid): Stand
}
return StandalonePostgresql::create([
'name' => generate_database_name('postgresql'),
'postgres_password' => \Illuminate\Support\Str::password(),
'postgres_password' => \Illuminate\Support\Str::password(symbols: false),
'environment_id' => $environment_id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),

View File

@@ -1,6 +1,7 @@
<?php
use App\Models\Server;
use App\Models\StandalonePostgresql;
use Symfony\Component\Yaml\Yaml;
function get_proxy_path()
@@ -166,3 +167,75 @@ function setup_default_redirect_404(string|null $redirect_url, Server $server)
}
}
}
function startPostgresProxy(StandalonePostgresql $database)
{
$containerName = "{$database->uuid}-proxy";
$configuration_dir = database_proxy_dir($database->uuid);
$nginxconf = <<<EOF
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
events {
worker_connections 1024;
}
stream {
server {
listen $database->public_port;
proxy_pass $database->uuid:5432;
}
}
EOF;
$docker_compose = [
'version' => '3.8',
'services' => [
$containerName => [
'image' => "nginx:stable-alpine",
'container_name' => $containerName,
'restart' => RESTART_MODE,
'volumes' => [
"$configuration_dir/nginx.conf:/etc/nginx/nginx.conf:ro",
],
'ports' => [
"$database->public_port:$database->public_port",
],
'networks' => [
$database->destination->network,
],
'healthcheck' => [
'test' => [
'CMD-SHELL',
'stat /etc/nginx/nginx.conf || exit 1',
],
'interval' => '5s',
'timeout' => '5s',
'retries' => 3,
'start_period' => '1s'
],
]
],
'networks' => [
$database->destination->network => [
'external' => true,
'name' => $database->destination->network,
'attachable' => true,
]
]
];
$dockercompose_base64 = base64_encode(Yaml::dump($docker_compose, 4, 2));
$nginxconf_base64 = base64_encode($nginxconf);
instant_remote_process([
"mkdir -p $configuration_dir",
"echo '{$nginxconf_base64}' | base64 -d > $configuration_dir/nginx.conf",
"echo '{$dockercompose_base64}' | base64 -d > $configuration_dir/docker-compose.yaml",
"docker compose --project-directory {$configuration_dir} up -d >/dev/null",
], $database->destination->server);
}
function stopPostgresProxy(StandalonePostgresql $database)
{
instant_remote_process(["docker rm -f {$database->uuid}-proxy"], $database->destination->server);
}

View File

@@ -28,6 +28,10 @@ function database_configuration_dir(): string
{
return '/data/coolify/databases';
}
function database_proxy_dir($uuid): string
{
return "/data/coolify/databases/$uuid/proxy";
}
function backup_dir(): string
{
@@ -53,9 +57,15 @@ function showBoarding(): bool
{
return currentTeam()->show_boarding ?? false;
}
function refreshSession(): void
function refreshSession(?Team $team = null): void
{
$team = Team::find(currentTeam()->id);
if (!$team) {
$team = Team::find(currentTeam()->id);
}
Cache::forget('team:' . auth()->user()->id);
Cache::remember('team:' . auth()->user()->id, 3600, function() use ($team) {
return $team;
});
session(['currentTeam' => $team]);
}
function general_error_handler(Throwable | null $err = null, $that = null, $isJson = false, $customErrorMessage = null): mixed
@@ -292,3 +302,25 @@ function setNotificationChannels($notifiable, $event)
}
return $channels;
}
function parseEnvFormatToArray($env_file_contents) {
$env_array = array();
$lines = explode("\n", $env_file_contents);
foreach ($lines as $line) {
if ($line === '' || substr($line, 0, 1) === '#') {
continue;
}
$equals_pos = strpos($line, '=');
if ($equals_pos !== false) {
$key = substr($line, 0, $equals_pos);
$value = substr($line, $equals_pos + 1);
if (substr($value, 0, 1) === '"' && substr($value, -1) === '"') {
$value = substr($value, 1, -1);
}
elseif (substr($value, 0, 1) === "'" && substr($value, -1) === "'") {
$value = substr($value, 1, -1);
}
$env_array[$key] = $value;
}
}
return $env_array;
}

View File

@@ -3,11 +3,11 @@
return [
// @see https://docs.sentry.io/product/sentry-basics/dsn-explainer/
'dsn' => 'https://abe219b6573947128ecf523c835f5f38@o1082494.ingest.sentry.io/4505347448045568',
'dsn' => 'https://62de992090e4e0cb28f18231835ea006@o1082494.ingest.sentry.io/4505347448045568',
// 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.24',
'release' => '4.0.0-beta.29',
'server_name' => env('APP_ID', 'coolify'),
// When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'),

View File

@@ -1,3 +1,3 @@
<?php
return '4.0.0-beta.24';
return '4.0.0-beta.29';

View File

@@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('teams', function (Blueprint $table) {
$table->text('telegram_notifications_test_message_thread_id')->nullable();
$table->text('telegram_notifications_deployments_message_thread_id')->nullable();
$table->text('telegram_notifications_status_changes_message_thread_id')->nullable();
$table->text('telegram_notifications_database_backups_message_thread_id')->nullable();
});
}
public function down(): void
{
Schema::table('teams', function (Blueprint $table) {
$table->dropColumn('telegram_message_thread_id');
$table->dropColumn('telegram_notifications_test_message_thread_id');
$table->dropColumn('telegram_notifications_deployments_message_thread_id');
$table->dropColumn('telegram_notifications_status_changes_message_thread_id');
$table->dropColumn('telegram_notifications_database_backups_message_thread_id');
});
}
};

View File

@@ -24,7 +24,7 @@
@endif
</span>
<input @disabled($disabled) type="checkbox" {{ $attributes->merge(['class' => $defaultClass]) }}
@if ($instantSave) wire:click='{{ $instantSave === 'instantSave' || $instantSave == '1' ? 'instantSave' : $instantSave }}'
@if ($instantSave) wire:loading.attr="disabled" wire:click='{{ $instantSave === 'instantSave' || $instantSave == '1' ? 'instantSave' : $instantSave }}'
wire:model.defer={{ $id }} @else wire:model.defer={{ $value ?? $id }} @endif />
</label>
</div>

View File

@@ -20,20 +20,31 @@
id="team.discord_webhook_url" label="Webhook" />
</form>
@if (data_get($team, 'discord_enabled'))
<h3 class="mt-4">Subscribe to events</h3>
<h2 class="mt-4">Subscribe to events</h2>
<div class="w-64">
@if (isDev())
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_test" label="Test" />
<h3 class="mt-4">Test</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_test" label="Enabled" />
</div>
@endif
<h4 class="mt-4">General</h4>
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_status_changes"
label="Container Status Changes" />
<h4 class="mt-4">Applications</h4>
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_deployments"
label="Deployments" />
<h4 class="mt-4">Databases</h4>
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_database_backups"
label="Backup Statuses" />
<h3 class="mt-4">Container Status Changes</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_status_changes"
label="Enabled" />
</div>
<h3 class="mt-4">Application Deployments</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_deployments"
label="Enabled" />
</div>
<h3 class="mt-4">Backup Status</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_database_backups"
label="Enabled" />
</div>
</div>
@endif
</div>

View File

@@ -21,7 +21,7 @@
Copy from Instance Settings
</x-forms.button>
@endif
@if (isEmailEnabled($team) || data_get($team, 'use_instance_email_settings'))
@if (isEmailEnabled($team) && auth()->user()->isAdminFromSession())
<x-forms.button onclick="sendTestEmail.showModal()"
class="text-white normal-case btn btn-xs no-animation btn-primary">
Send Test Email
@@ -36,12 +36,11 @@
label="Use hosted email service" />
</div>
@else
<div class="pb-4 w-96">
<x-forms.checkbox disabled id="team.use_instance_email_settings"
label="Use hosted email service (Pro+ subscription required)" />
</div>
<div class="pb-4 w-96">
<x-forms.checkbox disabled id="team.use_instance_email_settings"
label="Use hosted email service (Pro+ subscription required)" />
</div>
@endif
<h3 class="pb-4">Custom Email Service</h3>
@if (!$team->use_instance_email_settings)
<form class="flex flex-col items-end gap-2 pb-4 xl:flex-row" wire:submit.prevent='submitFromFields'>
<x-forms.input required id="team.smtp_from_name" helper="Name used in emails." label="From Name" />
@@ -110,19 +109,27 @@
</div>
@endif
@if (isEmailEnabled($team) || data_get($team, 'use_instance_email_settings'))
<h3 class="mt-4">Subscribe to events</h3>
<h2 class="mt-4">Subscribe to events</h2>
<div class="w-64">
@if (isDev())
<x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_test" label="Test" />
<h3 class="mt-4">Test</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_test" label="Enabled" />
</div>
@endif
<h4 class="mt-4">General</h4>
<x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_status_changes"
label="Container Status Changes" />
<h4 class="mt-4">Applications</h4>
<x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_deployments" label="Deployments" />
<h4 class="mt-4">Databases</h4>
<x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_database_backups"
label="Backup Statuses" />
<h3 class="mt-4">Container Status Changes</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_status_changes" label="Enabled" />
</div>
<h3 class="mt-4">Application Deployments</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_deployments" label="Enabled" />
</div>
<h3 class="mt-4">Backup Status</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_database_backups"
label="Enabled" />
</div>
</div>
@endif
</div>

View File

@@ -16,27 +16,50 @@
<x-forms.checkbox instantSave id="team.telegram_enabled" label="Notification Enabled" />
</div>
<div class="flex gap-2">
<x-forms.input type="password" helper="Get it from the <a class='inline-block text-white underline' href='https://t.me/botfather' target='_blank'>BotFather Bot</a> on Telegram." required
id="team.telegram_token" label="Token" />
<x-forms.input type="password" helper="Recommended to add your bot to a group chat and add its Chat ID here." required
<x-forms.input type="password"
helper="Get it from the <a class='inline-block text-white underline' href='https://t.me/botfather' target='_blank'>BotFather Bot</a> on Telegram."
required id="team.telegram_token" label="Token" />
<x-forms.input
helper="Recommended to add your bot to a group chat and add its Chat ID here." required
id="team.telegram_chat_id" label="Chat ID" />
</div>
</form>
@if (data_get($team, 'telegram_enabled'))
<h3 class="mt-4">Subscribe to events</h3>
<div class="w-64">
<h2 class="mt-4">Subscribe to events</h2>
<div class="w-96">
@if (isDev())
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_test" label="Test" />
<h3 class="mt-4">Test</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_test" label="Enabled" />
<x-forms.input
helper="If you are using Group chat with Topics, you can specify the topics ID. If empty, General topic will be used."
id="team.telegram_notifications_test_message_thread_id" label="Custom Topic ID" />
</div>
@endif
<h4 class="mt-4">General</h4>
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_status_changes"
label="Container Status Changes" />
<h4 class="mt-4">Applications</h4>
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_deployments"
label="Deployments" />
<h4 class="mt-4">Databases</h4>
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_database_backups"
label="Backup Statuses" />
<h3 class="mt-4">Container Status Changes</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_status_changes"
label="Enabled" />
<x-forms.input
helper="If you are using Group chat with Topics, you can specify the topics ID. If empty, General topic will be used."
id="team.telegram_notifications_status_changes_message_thread_id" label="Custom Topic ID" />
</div>
<h3 class="mt-4">Application Deployments</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_deployments"
label="Enabled" />
<x-forms.input
helper="If you are using Group chat with Topics, you can specify the topics ID. If empty, General topic will be used."
id="team.telegram_notifications_deployments_message_thread_id" label="Custom Topic ID" />
</div>
<h3 class="mt-4">Backup Status</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_database_backups"
label="Enabled" />
<x-forms.input
helper="If you are using Group chat with Topics, you can specify the topics ID. If empty, General topic will be used."
id="team.telegram_notifications_database_backups_message_thread_id" label="Custom Topic ID" />
</div>
</div>
@endif
@endif
</form>
</div>

View File

@@ -51,10 +51,15 @@
<x-forms.input label="Host Auth Method" id="database.postgres_host_auth_method"
placeholder="If empty, use default. See in docker docs." />
</div>
<div class="">
<div class="flex flex-col gap-2">
<h3 class="py-2">Network</h3>
<x-forms.input placeholder="3000:5432" id="database.ports_mappings" label="Ports Mappings"
<div class="flex items-end gap-2">
<x-forms.input placeholder="3000:5432" id="database.ports_mappings" label="Ports Mappings"
helper="A comma separated list of ports you would like to map to the host system.<br><span class='inline-block font-bold text-warning'>Example</span>3000:5432,3002:5433" />
<x-forms.input placeholder="5432" disabled="{{$database->is_public}}" id="database.public_port" label="Public Port" />
<x-forms.checkbox instantSave id="database.is_public" label="Accessible over the internet" />
</div>
<x-forms.input label="Postgres URL" readonly wire:model="db_url" />
</div>
</form>
<div class="pb-16">

View File

@@ -4,23 +4,40 @@
<h2>Environment Variables</h2>
<x-forms.button class="btn" onclick="newVariable.showModal()">+ Add</x-forms.button>
<livewire:project.shared.environment-variable.add />
<x-forms.button
wire:click='switch'>{{ $view === 'normal' ? 'Developer view' : 'Normal view' }}</x-forms.button>
</div>
<div>Environment (secrets) variables for this resource.</div>
<div>Environment variables (secrets) for this resource.</div>
</div>
@forelse ($resource->environment_variables as $env)
<livewire:project.shared.environment-variable.show wire:key="environment-{{ $env->id }}"
:env="$env" />
@empty
<div class="text-neutral-500">No environment variables found.</div>
@endforelse
@if ($resource->type() === 'application' && $resource->environment_variables_preview->count() > 0)
<div>
<h3>Preview Deployments</h3>
<div>Environment (secrets) variables for Preview Deployments.</div>
</div>
@foreach ($resource->environment_variables_preview as $env)
@if ($view === 'normal')
@forelse ($resource->environment_variables as $env)
<livewire:project.shared.environment-variable.show wire:key="environment-{{ $env->id }}"
:env="$env" />
@endforeach
@empty
<div class="text-neutral-500">No environment variables found.</div>
@endforelse
@if ($resource->type() === 'application' && $resource->environment_variables_preview->count() > 0 && $showPreview)
<div>
<h3>Preview Deployments</h3>
<div>Environment (secrets) variables for Preview Deployments.</div>
</div>
@foreach ($resource->environment_variables_preview as $env)
<livewire:project.shared.environment-variable.show wire:key="environment-{{ $env->id }}"
:env="$env" />
@endforeach
@endif
@else
<form wire:submit.prevent='saveVariables(false)' class="flex flex-col gap-2">
<x-forms.textarea rows=5 class="whitespace-pre-wrap" label="Environment Variables"
id="variables"></x-forms.textarea>
<x-forms.button type="submit" class="btn btn-primary">Save</x-forms.button>
</form>
@if ($showPreview)
<form wire:submit.prevent='saveVariables(true)' class="flex flex-col gap-2">
<x-forms.textarea rows=5 class="whitespace-pre-wrap" label="Preview Environment Variables"
id="variablesPreview"></x-forms.textarea>
<x-forms.button type="submit" class="btn btn-primary">Save</x-forms.button>
</form>
@endif
@endif
</div>

View File

@@ -22,12 +22,12 @@
<x-forms.button type="submit">
Save
</x-forms.button>
@if ($settings->resend_enabled || $settings->smtp_enabled)
<x-forms.button onclick="sendTestEmail.showModal()"
class="text-white normal-case btn btn-xs no-animation btn-primary">
Send Test Email
</x-forms.button>
@endif
@if (isEmailEnabled($settings))
<x-forms.button onclick="sendTestEmail.showModal()"
class="text-white normal-case btn btn-xs no-animation btn-primary">
Send Test Email
</x-forms.button>
@endif
</div>
</form>
<div class="flex flex-col gap-4">
@@ -67,14 +67,15 @@
<summary class="text-xl collapse-title">
<div>Resend</div>
<div class="w-32">
<x-forms.checkbox instantSave='submitResend' id="settings.resend_enabled" label="Enabled" />
<x-forms.checkbox instantSave='instantSaveResend' id="settings.resend_enabled" label="Enabled" />
</div>
</summary>
<div class="collapse-content">
<form wire:submit.prevent='submitResend' class="flex flex-col">
<div class="flex flex-col gap-4">
<div class="flex flex-col w-full gap-2 xl:flex-row">
<x-forms.input type="password" id="settings.resend_api_key" placeholder="API key" label="Host" />
<x-forms.input type="password" id="settings.resend_api_key" placeholder="API key"
label="Host" />
</div>
</div>
<div class="flex justify-end gap-4 pt-6">

View File

@@ -22,7 +22,7 @@
class="relative flex duration-300 transform transition ease-in-out max-w-md w-full pointer-events-auto {{ $position->is('center') ? 'text-center' : 'text-left' }}"
:class="toast.select({ error: 'text-white', info: 'text-white', success: 'text-white', warning: 'text-white' })"
>
<i class=" flex items-center gap-2 select-none not-italic pr-6 pl-4 py-3 rounded shadow-lg w-full {{ $alignment->is('bottom') ? 'mt-3' : 'mb-3' }}"
<i class=" flex items-center gap-2 select-none not-italic pr-6 pl-4 py-3 rounded shadow-lg w-full {{ $alignment->is('bottom') ? 'mt-3' : 'mb-3' }}"
:class="toast.select({
error: 'bg-coolgray-300',
info: 'bg-coolgray-300',
@@ -71,7 +71,7 @@
</svg>
</div>
</template>
<span x-html="toast.message" />
<span x-html="toast.message" class="w-full pr-10 break-words" />
</i>
@if ($closeable)

View File

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