feat: slack notifications

This commit is contained in:
Marvin von Rappard
2024-11-12 22:37:55 +01:00
parent 9af3fe9c97
commit eb0686fe20
27 changed files with 822 additions and 181 deletions

View File

@@ -0,0 +1,58 @@
<?php
namespace App\Jobs;
use App\Notifications\Dto\SlackMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
class SendMessageToSlackJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
private SlackMessage $message,
private string $webhookUrl
) {
}
public function handle(): void
{
Http::post($this->webhookUrl, [
'blocks' => [
[
'type' => 'section',
'text' => [
'type' => 'plain_text',
'text' => "Coolify Notification",
],
],
],
'attachments' => [
[
'color' => $this->message->color,
'blocks' => [
[
'type' => 'header',
'text' => [
'type' => 'plain_text',
'text' => $this->message->title,
],
],
[
'type' => 'section',
'text' => [
'type' => 'mrkdwn',
'text' => $this->message->description
]
]
]
]
]
]);
}
}

View File

@@ -0,0 +1,130 @@
<?php
namespace App\Livewire\Notifications;
use App\Models\Team;
use App\Notifications\Test;
use Livewire\Attributes\Validate;
use Livewire\Component;
class Slack extends Component
{
public Team $team;
#[Validate(['boolean'])]
public bool $slackEnabled = false;
#[Validate(['url', 'nullable'])]
public ?string $slackWebhookUrl = null;
#[Validate(['boolean'])]
public bool $slackNotificationsTest = false;
#[Validate(['boolean'])]
public bool $slackNotificationsDeployments = false;
#[Validate(['boolean'])]
public bool $slackNotificationsStatusChanges = false;
#[Validate(['boolean'])]
public bool $slackNotificationsDatabaseBackups = false;
#[Validate(['boolean'])]
public bool $slackNotificationsScheduledTasks = false;
#[Validate(['boolean'])]
public bool $slackNotificationsServerDiskUsage = false;
public function mount()
{
try {
$this->team = auth()->user()->currentTeam();
$this->syncData();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function syncData(bool $toModel = false)
{
if ($toModel) {
$this->validate();
$this->team->slack_enabled = $this->slackEnabled;
$this->team->slack_webhook_url = $this->slackWebhookUrl;
$this->team->slack_notifications_test = $this->slackNotificationsTest;
$this->team->slack_notifications_deployments = $this->slackNotificationsDeployments;
$this->team->slack_notifications_status_changes = $this->slackNotificationsStatusChanges;
$this->team->slack_notifications_database_backups = $this->slackNotificationsDatabaseBackups;
$this->team->slack_notifications_scheduled_tasks = $this->slackNotificationsScheduledTasks;
$this->team->slack_notifications_server_disk_usage = $this->slackNotificationsServerDiskUsage;
$this->team->save();
refreshSession();
} else {
$this->slackEnabled = $this->team->slack_enabled;
$this->slackWebhookUrl = $this->team->slack_webhook_url;
$this->slackNotificationsTest = $this->team->slack_notifications_test;
$this->slackNotificationsDeployments = $this->team->slack_notifications_deployments;
$this->slackNotificationsStatusChanges = $this->team->slack_notifications_status_changes;
$this->slackNotificationsDatabaseBackups = $this->team->slack_notifications_database_backups;
$this->slackNotificationsScheduledTasks = $this->team->slack_notifications_scheduled_tasks;
$this->slackNotificationsServerDiskUsage = $this->team->slack_notifications_server_disk_usage;
}
}
public function instantSaveSlackEnabled()
{
try {
$this->validate([
'slackWebhookUrl' => 'required',
], [
'slackWebhookUrl.required' => 'Slack Webhook URL is required.',
]);
$this->saveModel();
} catch (\Throwable $e) {
$this->slackEnabled = false;
return handleError($e, $this);
}
}
public function instantSave()
{
try {
$this->syncData(true);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function submit()
{
try {
$this->resetErrorBag();
$this->syncData(true);
$this->saveModel();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function saveModel()
{
$this->syncData(true);
refreshSession();
$this->dispatch('success', 'Settings saved.');
}
public function sendTestNotification()
{
try {
$this->team->notify(new Test);
$this->dispatch('success', 'Test notification sent.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.notifications.slack');
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Models;
use App\Notifications\Channels\SendsDiscord; use App\Notifications\Channels\SendsDiscord;
use App\Notifications\Channels\SendsEmail; use App\Notifications\Channels\SendsEmail;
use App\Notifications\Channels\SendsSlack;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
@@ -70,7 +71,7 @@ use OpenApi\Attributes as OA;
), ),
] ]
)] )]
class Team extends Model implements SendsDiscord, SendsEmail class Team extends Model implements SendsDiscord, SendsEmail, SendsSlack
{ {
use Notifiable; use Notifiable;
@@ -127,6 +128,11 @@ class Team extends Model implements SendsDiscord, SendsEmail
]; ];
} }
public function routeNotificationForSlack()
{
return data_get($this, 'slack_webhook_url', null);
}
public function getRecepients($notification) public function getRecepients($notification)
{ {
$recipients = data_get($notification, 'emails', null); $recipients = data_get($notification, 'emails', null);
@@ -161,7 +167,7 @@ class Team extends Model implements SendsDiscord, SendsEmail
return 9999999; return 9999999;
} }
$team = Team::find(currentTeam()->id); $team = Team::find(currentTeam()->id);
if (! $team) { if (!$team) {
return 0; return 0;
} }

View File

@@ -5,6 +5,7 @@ namespace App\Notifications\Application;
use App\Models\Application; use App\Models\Application;
use App\Models\ApplicationPreview; use App\Models\ApplicationPreview;
use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\DiscordMessage;
use App\Notifications\Dto\SlackMessage;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
@@ -44,7 +45,7 @@ class DeploymentFailed extends Notification implements ShouldQueue
if (str($this->fqdn)->explode(',')->count() > 1) { if (str($this->fqdn)->explode(',')->count() > 1) {
$this->fqdn = str($this->fqdn)->explode(',')->first(); $this->fqdn = str($this->fqdn)->explode(',')->first();
} }
$this->deployment_url = base_url()."/project/{$this->project_uuid}/".urlencode($this->environment_name)."/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}"; $this->deployment_url = base_url() . "/project/{$this->project_uuid}/" . urlencode($this->environment_name) . "/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}";
} }
public function via(object $notifiable): array public function via(object $notifiable): array
@@ -58,10 +59,10 @@ class DeploymentFailed extends Notification implements ShouldQueue
$pull_request_id = data_get($this->preview, 'pull_request_id', 0); $pull_request_id = data_get($this->preview, 'pull_request_id', 0);
$fqdn = $this->fqdn; $fqdn = $this->fqdn;
if ($pull_request_id === 0) { if ($pull_request_id === 0) {
$mail->subject('Coolify: Deployment failed of '.$this->application_name.'.'); $mail->subject('Coolify: Deployment failed of ' . $this->application_name . '.');
} else { } else {
$fqdn = $this->preview->fqdn; $fqdn = $this->preview->fqdn;
$mail->subject('Coolify: Deployment failed of pull request #'.$this->preview->pull_request_id.' of '.$this->application_name.'.'); $mail->subject('Coolify: Deployment failed of pull request #' . $this->preview->pull_request_id . ' of ' . $this->application_name . '.');
} }
$mail->view('emails.application-deployment-failed', [ $mail->view('emails.application-deployment-failed', [
'name' => $this->application_name, 'name' => $this->application_name,
@@ -78,7 +79,7 @@ class DeploymentFailed extends Notification implements ShouldQueue
if ($this->preview) { if ($this->preview) {
$message = new DiscordMessage( $message = new DiscordMessage(
title: ':cross_mark: Deployment failed', title: ':cross_mark: Deployment failed',
description: 'Pull request: '.$this->preview->pull_request_id, description: 'Pull request: ' . $this->preview->pull_request_id,
color: DiscordMessage::errorColor(), color: DiscordMessage::errorColor(),
isCritical: true, isCritical: true,
); );
@@ -87,13 +88,13 @@ class DeploymentFailed extends Notification implements ShouldQueue
$message->addField('Environment', $this->environment_name, true); $message->addField('Environment', $this->environment_name, true);
$message->addField('Name', $this->application_name, true); $message->addField('Name', $this->application_name, true);
$message->addField('Deployment Logs', '[Link]('.$this->deployment_url.')'); $message->addField('Deployment Logs', '[Link](' . $this->deployment_url . ')');
if ($this->fqdn) { if ($this->fqdn) {
$message->addField('Domain', $this->fqdn, true); $message->addField('Domain', $this->fqdn, true);
} }
} else { } else {
if ($this->fqdn) { if ($this->fqdn) {
$description = '[Open application]('.$this->fqdn.')'; $description = '[Open application](' . $this->fqdn . ')';
} else { } else {
$description = ''; $description = '';
} }
@@ -108,7 +109,7 @@ class DeploymentFailed extends Notification implements ShouldQueue
$message->addField('Environment', $this->environment_name, true); $message->addField('Environment', $this->environment_name, true);
$message->addField('Name', $this->application_name, true); $message->addField('Name', $this->application_name, true);
$message->addField('Deployment Logs', '[Link]('.$this->deployment_url.')'); $message->addField('Deployment Logs', '[Link](' . $this->deployment_url . ')');
} }
return $message; return $message;
@@ -117,9 +118,9 @@ class DeploymentFailed extends Notification implements ShouldQueue
public function toTelegram(): array public function toTelegram(): array
{ {
if ($this->preview) { if ($this->preview) {
$message = 'Coolify: Pull request #'.$this->preview->pull_request_id.' of '.$this->application_name.' ('.$this->preview->fqdn.') deployment failed: '; $message = 'Coolify: Pull request #' . $this->preview->pull_request_id . ' of ' . $this->application_name . ' (' . $this->preview->fqdn . ') deployment failed: ';
} else { } else {
$message = 'Coolify: Deployment failed of '.$this->application_name.' ('.$this->fqdn.'): '; $message = 'Coolify: Deployment failed of ' . $this->application_name . ' (' . $this->fqdn . '): ';
} }
$buttons[] = [ $buttons[] = [
'text' => 'Deployment logs', 'text' => 'Deployment logs',
@@ -133,4 +134,31 @@ class DeploymentFailed extends Notification implements ShouldQueue
], ],
]; ];
} }
public function toSlack(): SlackMessage
{
if ($this->preview) {
$title = "Pull request #{$this->preview->pull_request_id} deployment failed";
$description = "Pull request deployment failed for {$this->application_name}";
if ($this->preview->fqdn) {
$description .= "\nPreview URL: {$this->preview->fqdn}";
}
} else {
$title = "Deployment failed";
$description = "Deployment failed for {$this->application_name}";
if ($this->fqdn) {
$description .= "\nApplication URL: {$this->fqdn}";
}
}
$description .= "\n\n**Project:** " . data_get($this->application, 'environment.project.name');
$description .= "\n**Environment:** {$this->environment_name}";
$description .= "\n**Deployment Logs:** {$this->deployment_url}";
return new SlackMessage(
title: $title,
description: $description,
color: SlackMessage::errorColor()
);
}
} }

View File

@@ -9,7 +9,7 @@ use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification; use Illuminate\Notifications\Notification;
use App\Notifications\Dto\SlackMessage;
class DeploymentSuccess extends Notification implements ShouldQueue class DeploymentSuccess extends Notification implements ShouldQueue
{ {
use Queueable; use Queueable;
@@ -44,7 +44,7 @@ class DeploymentSuccess extends Notification implements ShouldQueue
if (str($this->fqdn)->explode(',')->count() > 1) { if (str($this->fqdn)->explode(',')->count() > 1) {
$this->fqdn = str($this->fqdn)->explode(',')->first(); $this->fqdn = str($this->fqdn)->explode(',')->first();
} }
$this->deployment_url = base_url()."/project/{$this->project_uuid}/".urlencode($this->environment_name)."/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}"; $this->deployment_url = base_url() . "/project/{$this->project_uuid}/" . urlencode($this->environment_name) . "/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}";
} }
public function via(object $notifiable): array public function via(object $notifiable): array
@@ -84,21 +84,21 @@ class DeploymentSuccess extends Notification implements ShouldQueue
if ($this->preview) { if ($this->preview) {
$message = new DiscordMessage( $message = new DiscordMessage(
title: ':white_check_mark: Preview deployment successful', title: ':white_check_mark: Preview deployment successful',
description: 'Pull request: '.$this->preview->pull_request_id, description: 'Pull request: ' . $this->preview->pull_request_id,
color: DiscordMessage::successColor(), color: DiscordMessage::successColor(),
); );
if ($this->preview->fqdn) { if ($this->preview->fqdn) {
$message->addField('Application', '[Link]('.$this->preview->fqdn.')'); $message->addField('Application', '[Link](' . $this->preview->fqdn . ')');
} }
$message->addField('Project', data_get($this->application, 'environment.project.name'), true); $message->addField('Project', data_get($this->application, 'environment.project.name'), true);
$message->addField('Environment', $this->environment_name, true); $message->addField('Environment', $this->environment_name, true);
$message->addField('Name', $this->application_name, true); $message->addField('Name', $this->application_name, true);
$message->addField('Deployment logs', '[Link]('.$this->deployment_url.')'); $message->addField('Deployment logs', '[Link](' . $this->deployment_url . ')');
} else { } else {
if ($this->fqdn) { if ($this->fqdn) {
$description = '[Open application]('.$this->fqdn.')'; $description = '[Open application](' . $this->fqdn . ')';
} else { } else {
$description = ''; $description = '';
} }
@@ -111,7 +111,7 @@ class DeploymentSuccess extends Notification implements ShouldQueue
$message->addField('Environment', $this->environment_name, true); $message->addField('Environment', $this->environment_name, true);
$message->addField('Name', $this->application_name, true); $message->addField('Name', $this->application_name, true);
$message->addField('Deployment logs', '[Link]('.$this->deployment_url.')'); $message->addField('Deployment logs', '[Link](' . $this->deployment_url . ')');
} }
return $message; return $message;
@@ -120,7 +120,7 @@ class DeploymentSuccess extends Notification implements ShouldQueue
public function toTelegram(): array public function toTelegram(): array
{ {
if ($this->preview) { if ($this->preview) {
$message = 'Coolify: New PR'.$this->preview->pull_request_id.' version successfully deployed of '.$this->application_name.''; $message = 'Coolify: New PR' . $this->preview->pull_request_id . ' version successfully deployed of ' . $this->application_name . '';
if ($this->preview->fqdn) { if ($this->preview->fqdn) {
$buttons[] = [ $buttons[] = [
'text' => 'Open Application', 'text' => 'Open Application',
@@ -128,7 +128,7 @@ class DeploymentSuccess extends Notification implements ShouldQueue
]; ];
} }
} else { } else {
$message = '✅ New version successfully deployed of '.$this->application_name.''; $message = '✅ New version successfully deployed of ' . $this->application_name . '';
if ($this->fqdn) { if ($this->fqdn) {
$buttons[] = [ $buttons[] = [
'text' => 'Open Application', 'text' => 'Open Application',
@@ -148,4 +148,32 @@ class DeploymentSuccess extends Notification implements ShouldQueue
], ],
]; ];
} }
public function toSlack(): SlackMessage
{
if ($this->preview) {
$title = "Pull request #{$this->preview->pull_request_id} successfully deployed";
$description = "New version successfully deployed for {$this->application_name}";
if ($this->preview->fqdn) {
$description .= "\nPreview URL: {$this->preview->fqdn}";
}
} else {
$title = "New version successfully deployed";
$description = "New version successfully deployed for {$this->application_name}";
if ($this->fqdn) {
$description .= "\nApplication URL: {$this->fqdn}";
}
}
$description .= "\n\n**Project:** " . data_get($this->application, 'environment.project.name');
$description .= "\n**Environment:** {$this->environment_name}";
$description .= "\n**Deployment Logs:** {$this->deployment_url}";
return new SlackMessage(
title: $title,
description: $description,
color: SlackMessage::successColor()
);
}
} }

View File

@@ -8,7 +8,7 @@ use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification; use Illuminate\Notifications\Notification;
use App\Notifications\Dto\SlackMessage;
class StatusChanged extends Notification implements ShouldQueue class StatusChanged extends Notification implements ShouldQueue
{ {
use Queueable; use Queueable;
@@ -34,7 +34,7 @@ class StatusChanged extends Notification implements ShouldQueue
if (str($this->fqdn)->explode(',')->count() > 1) { if (str($this->fqdn)->explode(',')->count() > 1) {
$this->fqdn = str($this->fqdn)->explode(',')->first(); $this->fqdn = str($this->fqdn)->explode(',')->first();
} }
$this->resource_url = base_url()."/project/{$this->project_uuid}/".urlencode($this->environment_name)."/application/{$this->resource->uuid}"; $this->resource_url = base_url() . "/project/{$this->project_uuid}/" . urlencode($this->environment_name) . "/application/{$this->resource->uuid}";
} }
public function via(object $notifiable): array public function via(object $notifiable): array
@@ -60,7 +60,7 @@ class StatusChanged extends Notification implements ShouldQueue
{ {
return new DiscordMessage( return new DiscordMessage(
title: ':cross_mark: Application stopped', title: ':cross_mark: Application stopped',
description: '[Open Application in Coolify]('.$this->resource_url.')', description: '[Open Application in Coolify](' . $this->resource_url . ')',
color: DiscordMessage::errorColor(), color: DiscordMessage::errorColor(),
isCritical: true, isCritical: true,
); );
@@ -68,7 +68,7 @@ class StatusChanged extends Notification implements ShouldQueue
public function toTelegram(): array public function toTelegram(): array
{ {
$message = 'Coolify: '.$this->resource_name.' has been stopped.'; $message = 'Coolify: ' . $this->resource_name . ' has been stopped.';
return [ return [
'message' => $message, 'message' => $message,
@@ -80,4 +80,20 @@ class StatusChanged extends Notification implements ShouldQueue
], ],
]; ];
} }
public function toSlack(): SlackMessage
{
$title = "Application stopped";
$description = "{$this->resource_name} has been stopped";
$description .= "\n\n**Project:** " . data_get($this->resource, 'environment.project.name');
$description .= "\n**Environment:** {$this->environment_name}";
$description .= "\n**Application URL:** {$this->resource_url}";
return new SlackMessage(
title: $title,
description: $description,
color: SlackMessage::errorColor()
);
}
} }

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Notifications\Channels;
interface SendsSlack
{
public function routeNotificationForSlack();
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Notifications\Channels;
use App\Jobs\SendMessageToSlackJob;
use Illuminate\Notifications\Notification;
class SlackChannel
{
/**
* Send the given notification.
*/
public function send(SendsSlack $notifiable, Notification $notification): void
{
$message = $notification->toSlack();
$webhookUrl = $notifiable->routeNotificationForSlack();
if (!$webhookUrl) {
return;
}
dispatch(new SendMessageToSlackJob($message, $webhookUrl))->onQueue('high');
}
}

View File

@@ -8,14 +8,16 @@ use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification; use Illuminate\Notifications\Notification;
use App\Notifications\Dto\SlackMessage;
class ContainerRestarted extends Notification implements ShouldQueue class ContainerRestarted extends Notification implements ShouldQueue
{ {
use Queueable; use Queueable;
public $tries = 1; public $tries = 1;
public function __construct(public string $name, public Server $server, public ?string $url = null) {} public function __construct(public string $name, public Server $server, public ?string $url = null)
{
}
public function via(object $notifiable): array public function via(object $notifiable): array
{ {
@@ -44,7 +46,7 @@ class ContainerRestarted extends Notification implements ShouldQueue
); );
if ($this->url) { if ($this->url) {
$message->addField('Resource', '[Link]('.$this->url.')'); $message->addField('Resource', '[Link](' . $this->url . ')');
} }
return $message; return $message;
@@ -69,4 +71,20 @@ class ContainerRestarted extends Notification implements ShouldQueue
return $payload; return $payload;
} }
public function toSlack(): SlackMessage
{
$title = "Resource restarted";
$description = "A resource ({$this->name}) has been restarted automatically on {$this->server->name}";
if ($this->url) {
$description .= "\n**Resource URL:** {$this->url}";
}
return new SlackMessage(
title: $title,
description: $description,
color: SlackMessage::warningColor()
);
}
} }

View File

@@ -8,14 +8,16 @@ use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification; use Illuminate\Notifications\Notification;
use App\Notifications\Dto\SlackMessage;
class ContainerStopped extends Notification implements ShouldQueue class ContainerStopped extends Notification implements ShouldQueue
{ {
use Queueable; use Queueable;
public $tries = 1; public $tries = 1;
public function __construct(public string $name, public Server $server, public ?string $url = null) {} public function __construct(public string $name, public Server $server, public ?string $url = null)
{
}
public function via(object $notifiable): array public function via(object $notifiable): array
{ {
@@ -44,7 +46,7 @@ class ContainerStopped extends Notification implements ShouldQueue
); );
if ($this->url) { if ($this->url) {
$message->addField('Resource', '[Link]('.$this->url.')'); $message->addField('Resource', '[Link](' . $this->url . ')');
} }
return $message; return $message;
@@ -69,4 +71,20 @@ class ContainerStopped extends Notification implements ShouldQueue
return $payload; return $payload;
} }
public function toSlack(): SlackMessage
{
$title = "Resource stopped";
$description = "A resource ({$this->name}) has been stopped unexpectedly on {$this->server->name}";
if ($this->url) {
$description .= "\n**Resource URL:** {$this->url}";
}
return new SlackMessage(
title: $title,
description: $description,
color: SlackMessage::errorColor()
);
}
} }

View File

@@ -8,7 +8,7 @@ use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification; use Illuminate\Notifications\Notification;
use App\Notifications\Dto\SlackMessage;
class BackupFailed extends Notification implements ShouldQueue class BackupFailed extends Notification implements ShouldQueue
{ {
use Queueable; use Queueable;
@@ -69,4 +69,19 @@ class BackupFailed extends Notification implements ShouldQueue
'message' => $message, 'message' => $message,
]; ];
} }
public function toSlack(): SlackMessage
{
$title = "Database backup failed";
$description = "Database backup for {$this->name} (db:{$this->database_name}) has FAILED.";
$description .= "\n\n**Frequency:** {$this->frequency}";
$description .= "\n\n**Error Output:**\n{$this->output}";
return new SlackMessage(
title: $title,
description: $description,
color: SlackMessage::errorColor()
);
}
} }

View File

@@ -8,7 +8,7 @@ use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification; use Illuminate\Notifications\Notification;
use App\Notifications\Dto\SlackMessage;
class BackupSuccess extends Notification implements ShouldQueue class BackupSuccess extends Notification implements ShouldQueue
{ {
use Queueable; use Queueable;
@@ -66,4 +66,18 @@ class BackupSuccess extends Notification implements ShouldQueue
'message' => $message, 'message' => $message,
]; ];
} }
public function toSlack(): SlackMessage
{
$title = "Database backup successful";
$description = "Database backup for {$this->name} (db:{$this->database_name}) was successful.";
$description .= "\n\n**Frequency:** {$this->frequency}";
return new SlackMessage(
title: $title,
description: $description,
color: SlackMessage::successColor()
);
}
} }

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Notifications\Dto;
class SlackMessage
{
public function __construct(
public string $title,
public string $description,
public string $color = '#0099ff'
) {
}
public static function infoColor(): string
{
return '#0099ff';
}
public static function errorColor(): string
{
return '#ff0000';
}
public static function successColor(): string
{
return '#00ff00';
}
}

View File

@@ -4,7 +4,9 @@ namespace App\Notifications\Internal;
use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\TelegramChannel; use App\Notifications\Channels\TelegramChannel;
use App\Notifications\Channels\SlackChannel;
use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\DiscordMessage;
use App\Notifications\Dto\SlackMessage;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification; use Illuminate\Notifications\Notification;
@@ -15,13 +17,16 @@ class GeneralNotification extends Notification implements ShouldQueue
public $tries = 1; public $tries = 1;
public function __construct(public string $message) {} public function __construct(public string $message)
{
}
public function via(object $notifiable): array public function via(object $notifiable): array
{ {
$channels = []; $channels = [];
$isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled'); $isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
$isSlackEnabled = data_get($notifiable, 'slack_enabled');
if ($isDiscordEnabled) { if ($isDiscordEnabled) {
$channels[] = DiscordChannel::class; $channels[] = DiscordChannel::class;
@@ -29,6 +34,9 @@ class GeneralNotification extends Notification implements ShouldQueue
if ($isTelegramEnabled) { if ($isTelegramEnabled) {
$channels[] = TelegramChannel::class; $channels[] = TelegramChannel::class;
} }
if ($isSlackEnabled) {
$channels[] = SlackChannel::class;
}
return $channels; return $channels;
} }
@@ -48,4 +56,13 @@ class GeneralNotification extends Notification implements ShouldQueue
'message' => $this->message, 'message' => $this->message,
]; ];
} }
public function toSlack(): SlackMessage
{
return new SlackMessage(
title: 'Coolify: General Notification',
description: $this->message,
color: SlackMessage::infoColor(),
);
}
} }

View File

@@ -8,7 +8,7 @@ use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification; use Illuminate\Notifications\Notification;
use App\Notifications\Dto\SlackMessage;
class TaskFailed extends Notification implements ShouldQueue class TaskFailed extends Notification implements ShouldQueue
{ {
use Queueable; use Queueable;
@@ -55,7 +55,7 @@ class TaskFailed extends Notification implements ShouldQueue
); );
if ($this->url) { if ($this->url) {
$message->addField('Scheduled task', '[Link]('.$this->url.')'); $message->addField('Scheduled task', '[Link](' . $this->url . ')');
} }
return $message; return $message;
@@ -75,4 +75,24 @@ class TaskFailed extends Notification implements ShouldQueue
'message' => $message, 'message' => $message,
]; ];
} }
public function toSlack(): SlackMessage
{
$title = "Scheduled task failed";
$description = "Scheduled task ({$this->task->name}) failed.";
if ($this->output) {
$description .= "\n\n**Error Output:**\n{$this->output}";
}
if ($this->url) {
$description .= "\n\n**Task URL:** {$this->url}";
}
return new SlackMessage(
title: $title,
description: $description,
color: SlackMessage::errorColor()
);
}
} }

View File

@@ -6,6 +6,7 @@ use App\Models\Server;
use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\TelegramChannel; use App\Notifications\Channels\TelegramChannel;
use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\DiscordMessage;
use App\Notifications\Dto\SlackMessage;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification; use Illuminate\Notifications\Notification;
@@ -16,7 +17,9 @@ class DockerCleanup extends Notification implements ShouldQueue
public $tries = 1; public $tries = 1;
public function __construct(public Server $server, public string $message) {} public function __construct(public Server $server, public string $message)
{
}
public function via(object $notifiable): array public function via(object $notifiable): array
{ {
@@ -24,7 +27,7 @@ class DockerCleanup extends Notification implements ShouldQueue
// $isEmailEnabled = isEmailEnabled($notifiable); // $isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled'); $isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
$isSlackEnabled = data_get($notifiable, 'slack_enabled');
if ($isDiscordEnabled) { if ($isDiscordEnabled) {
$channels[] = DiscordChannel::class; $channels[] = DiscordChannel::class;
} }
@@ -34,6 +37,9 @@ class DockerCleanup extends Notification implements ShouldQueue
if ($isTelegramEnabled) { if ($isTelegramEnabled) {
$channels[] = TelegramChannel::class; $channels[] = TelegramChannel::class;
} }
if ($isSlackEnabled) {
$channels[] = SlackChannel::class;
}
return $channels; return $channels;
} }
@@ -65,4 +71,13 @@ class DockerCleanup extends Notification implements ShouldQueue
'message' => "Coolify: Server '{$this->server->name}' cleanup job done!\n\n{$this->message}", 'message' => "Coolify: Server '{$this->server->name}' cleanup job done!\n\n{$this->message}",
]; ];
} }
public function toSlack(): SlackMessage
{
return new SlackMessage(
title: 'Server cleanup job done',
description: "Server '{$this->server->name}' cleanup job done!\n\n{$this->message}",
color: SlackMessage::successColor()
);
}
} }

View File

@@ -6,7 +6,9 @@ use App\Models\Server;
use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\TelegramChannel; use App\Notifications\Channels\TelegramChannel;
use App\Notifications\Channels\SlackChannel;
use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\DiscordMessage;
use App\Notifications\Dto\SlackMessage;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
@@ -18,7 +20,9 @@ class ForceDisabled extends Notification implements ShouldQueue
public $tries = 1; public $tries = 1;
public function __construct(public Server $server) {} public function __construct(public Server $server)
{
}
public function via(object $notifiable): array public function via(object $notifiable): array
{ {
@@ -26,7 +30,7 @@ class ForceDisabled extends Notification implements ShouldQueue
$isEmailEnabled = isEmailEnabled($notifiable); $isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled'); $isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
$isSlackEnabled = data_get($notifiable, 'slack_enabled');
if ($isDiscordEnabled) { if ($isDiscordEnabled) {
$channels[] = DiscordChannel::class; $channels[] = DiscordChannel::class;
} }
@@ -36,6 +40,9 @@ class ForceDisabled extends Notification implements ShouldQueue
if ($isTelegramEnabled) { if ($isTelegramEnabled) {
$channels[] = TelegramChannel::class; $channels[] = TelegramChannel::class;
} }
if ($isSlackEnabled) {
$channels[] = SlackChannel::class;
}
return $channels; return $channels;
} }
@@ -70,4 +77,19 @@ class ForceDisabled extends Notification implements ShouldQueue
'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' => "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).",
]; ];
} }
public function toSlack(): SlackMessage
{
$title = "Server disabled";
$description = "Server ({$this->server->name}) disabled because it is not paid!\n";
$description .= "All automations and integrations are stopped.\n\n";
$description .= "Please update your subscription to enable the server again: https://app.coolify.io/subscriptions";
return new SlackMessage(
title: $title,
description: $description,
color: SlackMessage::errorColor()
);
}
} }

View File

@@ -6,7 +6,9 @@ use App\Models\Server;
use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\TelegramChannel; use App\Notifications\Channels\TelegramChannel;
use App\Notifications\Channels\SlackChannel;
use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\DiscordMessage;
use App\Notifications\Dto\SlackMessage;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
@@ -18,7 +20,9 @@ class ForceEnabled extends Notification implements ShouldQueue
public $tries = 1; public $tries = 1;
public function __construct(public Server $server) {} public function __construct(public Server $server)
{
}
public function via(object $notifiable): array public function via(object $notifiable): array
{ {
@@ -26,7 +30,7 @@ class ForceEnabled extends Notification implements ShouldQueue
$isEmailEnabled = isEmailEnabled($notifiable); $isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled'); $isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
$isSlackEnabled = data_get($notifiable, 'slack_enabled');
if ($isDiscordEnabled) { if ($isDiscordEnabled) {
$channels[] = DiscordChannel::class; $channels[] = DiscordChannel::class;
} }
@@ -36,6 +40,9 @@ class ForceEnabled extends Notification implements ShouldQueue
if ($isTelegramEnabled) { if ($isTelegramEnabled) {
$channels[] = TelegramChannel::class; $channels[] = TelegramChannel::class;
} }
if ($isSlackEnabled) {
$channels[] = SlackChannel::class;
}
return $channels; return $channels;
} }
@@ -66,4 +73,15 @@ class ForceEnabled extends Notification implements ShouldQueue
'message' => "Coolify: Server ({$this->server->name}) enabled again!", 'message' => "Coolify: Server ({$this->server->name}) enabled again!",
]; ];
} }
public function toSlack(): SlackMessage
{
return new SlackMessage(
title: 'Server enabled',
description: "Server '{$this->server->name}' enabled again!",
color: SlackMessage::successColor()
);
}
} }

View File

@@ -4,6 +4,7 @@ namespace App\Notifications\Server;
use App\Models\Server; use App\Models\Server;
use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\DiscordMessage;
use App\Notifications\Dto\SlackMessage;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
@@ -15,7 +16,9 @@ class HighDiskUsage extends Notification implements ShouldQueue
public $tries = 1; public $tries = 1;
public function __construct(public Server $server, public int $disk_usage, public int $server_disk_usage_notification_threshold) {} public function __construct(public Server $server, public int $disk_usage, public int $server_disk_usage_notification_threshold)
{
}
public function via(object $notifiable): array public function via(object $notifiable): array
{ {
@@ -47,7 +50,7 @@ class HighDiskUsage extends Notification implements ShouldQueue
$message->addField('Disk usage', "{$this->disk_usage}%", true); $message->addField('Disk usage', "{$this->disk_usage}%", true);
$message->addField('Threshold', "{$this->server_disk_usage_notification_threshold}%", 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('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)'); $message->addField('Change Settings', '[Threshold](' . base_url() . '/server/' . $this->server->uuid . '#advanced) | [Notification](' . base_url() . '/notifications/discord)');
return $message; return $message;
} }
@@ -58,4 +61,22 @@ class HighDiskUsage extends Notification implements ShouldQueue
'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.", '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.",
]; ];
} }
public function toSlack(): SlackMessage
{
$description = "Server '{$this->server->name}' high disk usage detected!\n";
$description .= "Disk usage: {$this->disk_usage}%\n";
$description .= "Threshold: {$this->server_disk_usage_notification_threshold}%\n\n";
$description .= "Please cleanup your disk to prevent data-loss.\n";
$description .= "Tips for cleanup: https://coolify.io/docs/knowledge-base/server/automated-cleanup\n";
$description .= "Change settings:\n";
$description .= "- Threshold: " . base_url() . "/server/" . $this->server->uuid . "#advanced\n";
$description .= "- Notifications: " . base_url() . "/notifications/discord";
return new SlackMessage(
title: 'High disk usage detected',
description: $description,
color: SlackMessage::errorColor()
);
}
} }

View File

@@ -6,7 +6,9 @@ use App\Models\Server;
use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\TelegramChannel; use App\Notifications\Channels\TelegramChannel;
use App\Notifications\Channels\SlackChannel;
use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\DiscordMessage;
use App\Notifications\Dto\SlackMessage;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
@@ -23,7 +25,7 @@ class Reachable extends Notification implements ShouldQueue
public function __construct(public Server $server) public function __construct(public Server $server)
{ {
$this->isRateLimited = isEmailRateLimited( $this->isRateLimited = isEmailRateLimited(
limiterKey: 'server-reachable:'.$this->server->id, limiterKey: 'server-reachable:' . $this->server->id,
); );
} }
@@ -37,7 +39,7 @@ class Reachable extends Notification implements ShouldQueue
$isEmailEnabled = isEmailEnabled($notifiable); $isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled'); $isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
$isSlackEnabled = data_get($notifiable, 'slack_enabled');
if ($isDiscordEnabled) { if ($isDiscordEnabled) {
$channels[] = DiscordChannel::class; $channels[] = DiscordChannel::class;
} }
@@ -47,6 +49,9 @@ class Reachable extends Notification implements ShouldQueue
if ($isTelegramEnabled) { if ($isTelegramEnabled) {
$channels[] = TelegramChannel::class; $channels[] = TelegramChannel::class;
} }
if ($isSlackEnabled) {
$channels[] = SlackChannel::class;
}
return $channels; return $channels;
} }
@@ -77,4 +82,16 @@ class Reachable extends Notification implements ShouldQueue
'message' => "Coolify: Server '{$this->server->name}' revived. All automations & integrations are turned on again!", 'message' => "Coolify: Server '{$this->server->name}' revived. All automations & integrations are turned on again!",
]; ];
} }
public function toSlack(): SlackMessage
{
return new SlackMessage(
title: "Server revived",
description: "Server '{$this->server->name}' revived.\nAll automations & integrations are turned on again!",
color: SlackMessage::successColor()
);
}
} }

View File

@@ -6,7 +6,9 @@ use App\Models\Server;
use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\TelegramChannel; use App\Notifications\Channels\TelegramChannel;
use App\Notifications\Channels\SlackChannel;
use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\DiscordMessage;
use App\Notifications\Dto\SlackMessage;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
@@ -23,7 +25,7 @@ class Unreachable extends Notification implements ShouldQueue
public function __construct(public Server $server) public function __construct(public Server $server)
{ {
$this->isRateLimited = isEmailRateLimited( $this->isRateLimited = isEmailRateLimited(
limiterKey: 'server-unreachable:'.$this->server->id, limiterKey: 'server-unreachable:' . $this->server->id,
); );
} }
@@ -37,6 +39,7 @@ class Unreachable extends Notification implements ShouldQueue
$isEmailEnabled = isEmailEnabled($notifiable); $isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled'); $isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
$isSlackEnabled = data_get($notifiable, 'slack_enabled');
if ($isDiscordEnabled) { if ($isDiscordEnabled) {
$channels[] = DiscordChannel::class; $channels[] = DiscordChannel::class;
@@ -47,6 +50,9 @@ class Unreachable extends Notification implements ShouldQueue
if ($isTelegramEnabled) { if ($isTelegramEnabled) {
$channels[] = TelegramChannel::class; $channels[] = TelegramChannel::class;
} }
if ($isSlackEnabled) {
$channels[] = SlackChannel::class;
}
return $channels; return $channels;
} }
@@ -81,4 +87,18 @@ class Unreachable extends Notification implements ShouldQueue
'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' => "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.",
]; ];
} }
public function toSlack(): SlackMessage
{
$description = "Your server '{$this->server->name}' is unreachable.\n";
$description .= "All automations & integrations are turned off!\n\n";
$description .= "*IMPORTANT:* We automatically try to revive your server and turn on all automations & integrations.";
return new SlackMessage(
title: 'Server unreachable',
description: $description,
color: SlackMessage::errorColor()
);
}
} }

View File

@@ -3,6 +3,7 @@
namespace App\Notifications; namespace App\Notifications;
use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\DiscordMessage;
use App\Notifications\Dto\SlackMessage;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
@@ -15,7 +16,9 @@ class Test extends Notification implements ShouldQueue
public $tries = 5; public $tries = 5;
public function __construct(public ?string $emails = null) {} public function __construct(public ?string $emails = null)
{
}
public function via(object $notifiable): array public function via(object $notifiable): array
{ {
@@ -47,7 +50,7 @@ class Test extends Notification implements ShouldQueue
color: DiscordMessage::successColor(), color: DiscordMessage::successColor(),
); );
$message->addField(name: 'Dashboard', value: '[Link]('.base_url().')', inline: true); $message->addField(name: 'Dashboard', value: '[Link](' . base_url() . ')', inline: true);
return $message; return $message;
} }
@@ -64,4 +67,12 @@ class Test extends Notification implements ShouldQueue
], ],
]; ];
} }
public function toSlack(): SlackMessage
{
return new SlackMessage(
title: 'Test Slack Notification',
description: 'This is a test Slack notification from Coolify.'
);
}
} }

View File

@@ -26,6 +26,7 @@ use App\Models\Team;
use App\Models\User; use App\Models\User;
use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\SlackChannel;
use App\Notifications\Channels\TelegramChannel; use App\Notifications\Channels\TelegramChannel;
use App\Notifications\Internal\GeneralNotification; use App\Notifications\Internal\GeneralNotification;
use Carbon\CarbonImmutable; use Carbon\CarbonImmutable;
@@ -66,27 +67,27 @@ function base_configuration_dir(): string
} }
function application_configuration_dir(): string function application_configuration_dir(): string
{ {
return base_configuration_dir().'/applications'; return base_configuration_dir() . '/applications';
} }
function service_configuration_dir(): string function service_configuration_dir(): string
{ {
return base_configuration_dir().'/services'; return base_configuration_dir() . '/services';
} }
function database_configuration_dir(): string function database_configuration_dir(): string
{ {
return base_configuration_dir().'/databases'; return base_configuration_dir() . '/databases';
} }
function database_proxy_dir($uuid): string function database_proxy_dir($uuid): string
{ {
return base_configuration_dir()."/databases/$uuid/proxy"; return base_configuration_dir() . "/databases/$uuid/proxy";
} }
function backup_dir(): string function backup_dir(): string
{ {
return base_configuration_dir().'/backups'; return base_configuration_dir() . '/backups';
} }
function metrics_dir(): string function metrics_dir(): string
{ {
return base_configuration_dir().'/metrics'; return base_configuration_dir() . '/metrics';
} }
function generate_readme_file(string $name, string $updated_at): string function generate_readme_file(string $name, string $updated_at): string
@@ -114,15 +115,15 @@ function showBoarding(): bool
} }
function refreshSession(?Team $team = null): void function refreshSession(?Team $team = null): void
{ {
if (! $team) { if (!$team) {
if (Auth::user()->currentTeam()) { if (Auth::user()->currentTeam()) {
$team = Team::find(Auth::user()->currentTeam()->id); $team = Team::find(Auth::user()->currentTeam()->id);
} else { } else {
$team = User::find(Auth::id())->teams->first(); $team = User::find(Auth::id())->teams->first();
} }
} }
Cache::forget('team:'.Auth::id()); Cache::forget('team:' . Auth::id());
Cache::remember('team:'.Auth::id(), 3600, function () use ($team) { Cache::remember('team:' . Auth::id(), 3600, function () use ($team) {
return $team; return $team;
}); });
session(['currentTeam' => $team]); session(['currentTeam' => $team]);
@@ -154,7 +155,7 @@ function handleError(?Throwable $error = null, ?Livewire\Component $livewire = n
$message = null; $message = null;
} }
if ($customErrorMessage) { if ($customErrorMessage) {
$message = $customErrorMessage.' '.$message; $message = $customErrorMessage . ' ' . $message;
} }
if (isset($livewire)) { if (isset($livewire)) {
@@ -227,7 +228,7 @@ function generateSSHKey(string $type = 'rsa')
function formatPrivateKey(string $privateKey) function formatPrivateKey(string $privateKey)
{ {
$privateKey = trim($privateKey); $privateKey = trim($privateKey);
if (! str_ends_with($privateKey, "\n")) { if (!str_ends_with($privateKey, "\n")) {
$privateKey .= "\n"; $privateKey .= "\n";
} }
@@ -249,7 +250,7 @@ function is_transactional_emails_active(): bool
function set_transanctional_email_settings(?InstanceSettings $settings = null): ?string function set_transanctional_email_settings(?InstanceSettings $settings = null): ?string
{ {
if (! $settings) { if (!$settings) {
$settings = instanceSettings(); $settings = instanceSettings();
} }
config()->set('mail.from.address', data_get($settings, 'smtp_from_address')); config()->set('mail.from.address', data_get($settings, 'smtp_from_address'));
@@ -349,7 +350,7 @@ function isSubscribed()
function isProduction(): bool function isProduction(): bool
{ {
return ! isDev(); return !isDev();
} }
function isDev(): bool function isDev(): bool
{ {
@@ -358,7 +359,7 @@ function isDev(): bool
function isCloud(): bool function isCloud(): bool
{ {
return ! config('coolify.self_hosted'); return !config('coolify.self_hosted');
} }
function translate_cron_expression($expression_to_validate): string function translate_cron_expression($expression_to_validate): string
@@ -397,14 +398,14 @@ function send_user_an_email(MailMessage $mail, string $email, ?string $cc = null
{ {
$settings = instanceSettings(); $settings = instanceSettings();
$type = set_transanctional_email_settings($settings); $type = set_transanctional_email_settings($settings);
if (! $type) { if (!$type) {
throw new Exception('No email settings found.'); throw new Exception('No email settings found.');
} }
if ($cc) { if ($cc) {
Mail::send( Mail::send(
[], [],
[], [],
fn (Message $message) => $message fn(Message $message) => $message
->to($email) ->to($email)
->replyTo($email) ->replyTo($email)
->cc($cc) ->cc($cc)
@@ -415,7 +416,7 @@ function send_user_an_email(MailMessage $mail, string $email, ?string $cc = null
Mail::send( Mail::send(
[], [],
[], [],
fn (Message $message) => $message fn(Message $message) => $message
->to($email) ->to($email)
->subject($mail->subject) ->subject($mail->subject)
->html((string) $mail->render()) ->html((string) $mail->render())
@@ -440,11 +441,13 @@ function setNotificationChannels($notifiable, $event)
{ {
$channels = []; $channels = [];
$isEmailEnabled = isEmailEnabled($notifiable); $isEmailEnabled = isEmailEnabled($notifiable);
$isSlackEnabled = data_get($notifiable, 'slack_enabled');
$isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled'); $isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
$isSubscribedToEmailEvent = data_get($notifiable, "smtp_notifications_$event"); $isSubscribedToEmailEvent = data_get($notifiable, "smtp_notifications_$event");
$isSubscribedToDiscordEvent = data_get($notifiable, "discord_notifications_$event"); $isSubscribedToDiscordEvent = data_get($notifiable, "discord_notifications_$event");
$isSubscribedToTelegramEvent = data_get($notifiable, "telegram_notifications_$event"); $isSubscribedToTelegramEvent = data_get($notifiable, "telegram_notifications_$event");
$isSubscribedToSlackEvent = data_get($notifiable, "slack_notifications_$event");
if ($isDiscordEnabled && $isSubscribedToDiscordEvent) { if ($isDiscordEnabled && $isSubscribedToDiscordEvent) {
$channels[] = DiscordChannel::class; $channels[] = DiscordChannel::class;
@@ -455,6 +458,9 @@ function setNotificationChannels($notifiable, $event)
if ($isTelegramEnabled && $isSubscribedToTelegramEvent) { if ($isTelegramEnabled && $isSubscribedToTelegramEvent) {
$channels[] = TelegramChannel::class; $channels[] = TelegramChannel::class;
} }
if ($isSlackEnabled && $isSubscribedToSlackEvent) {
$channels[] = SlackChannel::class;
}
return $channels; return $channels;
} }
@@ -559,7 +565,7 @@ function getResourceByUuid(string $uuid, ?int $teamId = null)
return null; return null;
} }
$resource = queryResourcesByUuid($uuid); $resource = queryResourcesByUuid($uuid);
if (! is_null($resource) && $resource->environment->project->team_id === $teamId) { if (!is_null($resource) && $resource->environment->project->team_id === $teamId) {
return $resource; return $resource;
} }
@@ -651,29 +657,29 @@ function queryResourcesByUuid(string $uuid)
function generateTagDeployWebhook($tag_name) function generateTagDeployWebhook($tag_name)
{ {
$baseUrl = base_url(); $baseUrl = base_url();
$api = Url::fromString($baseUrl).'/api/v1'; $api = Url::fromString($baseUrl) . '/api/v1';
$endpoint = "/deploy?tag=$tag_name"; $endpoint = "/deploy?tag=$tag_name";
return $api.$endpoint; return $api . $endpoint;
} }
function generateDeployWebhook($resource) function generateDeployWebhook($resource)
{ {
$baseUrl = base_url(); $baseUrl = base_url();
$api = Url::fromString($baseUrl).'/api/v1'; $api = Url::fromString($baseUrl) . '/api/v1';
$endpoint = '/deploy'; $endpoint = '/deploy';
$uuid = data_get($resource, 'uuid'); $uuid = data_get($resource, 'uuid');
return $api.$endpoint."?uuid=$uuid&force=false"; return $api . $endpoint . "?uuid=$uuid&force=false";
} }
function generateGitManualWebhook($resource, $type) function generateGitManualWebhook($resource, $type)
{ {
if ($resource->source_id !== 0 && ! is_null($resource->source_id)) { if ($resource->source_id !== 0 && !is_null($resource->source_id)) {
return null; return null;
} }
if ($resource->getMorphClass() === \App\Models\Application::class) { if ($resource->getMorphClass() === \App\Models\Application::class) {
$baseUrl = base_url(); $baseUrl = base_url();
return Url::fromString($baseUrl)."/webhooks/source/$type/events/manual"; return Url::fromString($baseUrl) . "/webhooks/source/$type/events/manual";
} }
return null; return null;
@@ -700,7 +706,7 @@ function getTopLevelNetworks(Service|Application $resource)
$hasHostNetworkMode = data_get($service, 'network_mode') === 'host' ? true : false; $hasHostNetworkMode = data_get($service, 'network_mode') === 'host' ? true : false;
// Only add 'networks' key if 'network_mode' is not 'host' // Only add 'networks' key if 'network_mode' is not 'host'
if (! $hasHostNetworkMode) { if (!$hasHostNetworkMode) {
// Collect/create/update networks // Collect/create/update networks
if ($serviceNetworks->count() > 0) { if ($serviceNetworks->count() > 0) {
foreach ($serviceNetworks as $networkName => $networkDetails) { foreach ($serviceNetworks as $networkName => $networkDetails) {
@@ -714,7 +720,7 @@ function getTopLevelNetworks(Service|Application $resource)
$networkExists = $topLevelNetworks->contains(function ($value, $key) use ($networkName) { $networkExists = $topLevelNetworks->contains(function ($value, $key) use ($networkName) {
return $value == $networkName || $key == $networkName; return $value == $networkName || $key == $networkName;
}); });
if (! $networkExists) { if (!$networkExists) {
if (is_string($networkDetails) || is_int($networkDetails)) { if (is_string($networkDetails) || is_int($networkDetails)) {
$topLevelNetworks->put($networkDetails, null); $topLevelNetworks->put($networkDetails, null);
} }
@@ -725,7 +731,7 @@ function getTopLevelNetworks(Service|Application $resource)
$definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) { $definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) {
return $value == $definedNetwork; return $value == $definedNetwork;
}); });
if (! $definedNetworkExists) { if (!$definedNetworkExists) {
foreach ($definedNetwork as $network) { foreach ($definedNetwork as $network) {
$topLevelNetworks->put($network, [ $topLevelNetworks->put($network, [
'name' => $network, 'name' => $network,
@@ -766,7 +772,7 @@ function getTopLevelNetworks(Service|Application $resource)
$networkExists = $topLevelNetworks->contains(function ($value, $key) use ($networkName) { $networkExists = $topLevelNetworks->contains(function ($value, $key) use ($networkName) {
return $value == $networkName || $key == $networkName; return $value == $networkName || $key == $networkName;
}); });
if (! $networkExists) { if (!$networkExists) {
if (is_string($networkDetails) || is_int($networkDetails)) { if (is_string($networkDetails) || is_int($networkDetails)) {
$topLevelNetworks->put($networkDetails, null); $topLevelNetworks->put($networkDetails, null);
} }
@@ -776,7 +782,7 @@ function getTopLevelNetworks(Service|Application $resource)
$definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) { $definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) {
return $value == $definedNetwork; return $value == $definedNetwork;
}); });
if (! $definedNetworkExists) { if (!$definedNetworkExists) {
foreach ($definedNetwork as $network) { foreach ($definedNetwork as $network) {
$topLevelNetworks->put($network, [ $topLevelNetworks->put($network, [
'name' => $network, 'name' => $network,
@@ -912,7 +918,7 @@ function generateEnvValue(string $command, Service|Application|null $service = n
case 'PASSWORD_64': case 'PASSWORD_64':
$generatedValue = Str::password(length: 64, symbols: false); $generatedValue = Str::password(length: 64, symbols: false);
break; break;
// This is not base64, it's just a random string // This is not base64, it's just a random string
case 'BASE64_64': case 'BASE64_64':
$generatedValue = Str::random(64); $generatedValue = Str::random(64);
break; break;
@@ -923,7 +929,7 @@ function generateEnvValue(string $command, Service|Application|null $service = n
case 'BASE64_32': case 'BASE64_32':
$generatedValue = Str::random(32); $generatedValue = Str::random(32);
break; break;
// This is base64, // This is base64,
case 'REALBASE64_64': case 'REALBASE64_64':
$generatedValue = base64_encode(Str::random(64)); $generatedValue = base64_encode(Str::random(64));
break; break;
@@ -1014,7 +1020,7 @@ function validate_dns_entry(string $fqdn, Server $server)
} }
$settings = instanceSettings(); $settings = instanceSettings();
$is_dns_validation_enabled = data_get($settings, 'is_dns_validation_enabled'); $is_dns_validation_enabled = data_get($settings, 'is_dns_validation_enabled');
if (! $is_dns_validation_enabled) { if (!$is_dns_validation_enabled) {
return true; return true;
} }
$dns_servers = data_get($settings, 'custom_dns_servers'); $dns_servers = data_get($settings, 'custom_dns_servers');
@@ -1032,7 +1038,7 @@ function validate_dns_entry(string $fqdn, Server $server)
$query = new DNSQuery($dns_server); $query = new DNSQuery($dns_server);
$results = $query->query($host, $type); $results = $query->query($host, $type);
if ($results === false || $query->hasError()) { if ($results === false || $query->hasError()) {
ray('Error: '.$query->getLasterror()); ray('Error: ' . $query->getLasterror());
} else { } else {
foreach ($results as $result) { foreach ($results as $result) {
if ($result->getType() == $type) { if ($result->getType() == $type) {
@@ -1042,7 +1048,7 @@ function validate_dns_entry(string $fqdn, Server $server)
break; break;
} }
if ($result->getData() === $ip) { if ($result->getData() === $ip) {
ray($host.' has IP address '.$result->getData()); ray($host . ' has IP address ' . $result->getData());
ray($result->getString()); ray($result->getString());
$found_matching_ip = true; $found_matching_ip = true;
break; break;
@@ -1090,15 +1096,15 @@ function checkIfDomainIsAlreadyUsed(Collection|array $domains, ?string $teamId =
$applications = Application::ownedByCurrentTeamAPI($teamId)->get(['fqdn', 'uuid']); $applications = Application::ownedByCurrentTeamAPI($teamId)->get(['fqdn', 'uuid']);
$serviceApplications = ServiceApplication::ownedByCurrentTeamAPI($teamId)->get(['fqdn', 'uuid']); $serviceApplications = ServiceApplication::ownedByCurrentTeamAPI($teamId)->get(['fqdn', 'uuid']);
if ($uuid) { if ($uuid) {
$applications = $applications->filter(fn ($app) => $app->uuid !== $uuid); $applications = $applications->filter(fn($app) => $app->uuid !== $uuid);
$serviceApplications = $serviceApplications->filter(fn ($app) => $app->uuid !== $uuid); $serviceApplications = $serviceApplications->filter(fn($app) => $app->uuid !== $uuid);
} }
$domainFound = false; $domainFound = false;
foreach ($applications as $app) { foreach ($applications as $app) {
if (is_null($app->fqdn)) { if (is_null($app->fqdn)) {
continue; continue;
} }
$list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== ''); $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn($fqdn) => $fqdn !== '');
foreach ($list_of_domains as $domain) { foreach ($list_of_domains as $domain) {
if (str($domain)->endsWith('/')) { if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/'); $domain = str($domain)->beforeLast('/');
@@ -1117,7 +1123,7 @@ function checkIfDomainIsAlreadyUsed(Collection|array $domains, ?string $teamId =
if (str($app->fqdn)->isEmpty()) { if (str($app->fqdn)->isEmpty()) {
continue; continue;
} }
$list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== ''); $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn($fqdn) => $fqdn !== '');
foreach ($list_of_domains as $domain) { foreach ($list_of_domains as $domain) {
if (str($domain)->endsWith('/')) { if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/'); $domain = str($domain)->beforeLast('/');
@@ -1167,7 +1173,7 @@ function check_domain_usage(ServiceApplication|Application|null $resource = null
}); });
$apps = Application::all(); $apps = Application::all();
foreach ($apps as $app) { foreach ($apps as $app) {
$list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== ''); $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn($fqdn) => $fqdn !== '');
foreach ($list_of_domains as $domain) { foreach ($list_of_domains as $domain) {
if (str($domain)->endsWith('/')) { if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/'); $domain = str($domain)->beforeLast('/');
@@ -1186,7 +1192,7 @@ function check_domain_usage(ServiceApplication|Application|null $resource = null
} }
$apps = ServiceApplication::all(); $apps = ServiceApplication::all();
foreach ($apps as $app) { foreach ($apps as $app) {
$list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== ''); $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn($fqdn) => $fqdn !== '');
foreach ($list_of_domains as $domain) { foreach ($list_of_domains as $domain) {
if (str($domain)->endsWith('/')) { if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/'); $domain = str($domain)->beforeLast('/');
@@ -1222,7 +1228,7 @@ function parseCommandsByLineForSudo(Collection $commands, Server $server): array
{ {
$commands = $commands->map(function ($line) { $commands = $commands->map(function ($line) {
if ( if (
! str(trim($line))->startsWith([ !str(trim($line))->startsWith([
'cd', 'cd',
'command', 'command',
'echo', 'echo',
@@ -1243,7 +1249,7 @@ function parseCommandsByLineForSudo(Collection $commands, Server $server): array
$commands = $commands->map(function ($line) use ($server) { $commands = $commands->map(function ($line) use ($server) {
if (Str::startsWith($line, 'sudo mkdir -p')) { if (Str::startsWith($line, 'sudo mkdir -p')) {
return "$line && sudo chown -R $server->user:$server->user ".Str::after($line, 'sudo mkdir -p').' && sudo chmod -R o-rwx '.Str::after($line, 'sudo mkdir -p'); return "$line && sudo chown -R $server->user:$server->user " . Str::after($line, 'sudo mkdir -p') . ' && sudo chmod -R o-rwx ' . Str::after($line, 'sudo mkdir -p');
} }
return $line; return $line;
@@ -1271,11 +1277,11 @@ function parseCommandsByLineForSudo(Collection $commands, Server $server): array
} }
function parseLineForSudo(string $command, Server $server): string function parseLineForSudo(string $command, Server $server): string
{ {
if (! str($command)->startSwith('cd') && ! str($command)->startSwith('command')) { if (!str($command)->startSwith('cd') && !str($command)->startSwith('command')) {
$command = "sudo $command"; $command = "sudo $command";
} }
if (Str::startsWith($command, 'sudo mkdir -p')) { if (Str::startsWith($command, 'sudo mkdir -p')) {
$command = "$command && sudo chown -R $server->user:$server->user ".Str::after($command, 'sudo mkdir -p').' && sudo chmod -R o-rwx '.Str::after($command, 'sudo mkdir -p'); $command = "$command && sudo chown -R $server->user:$server->user " . Str::after($command, 'sudo mkdir -p') . ' && sudo chmod -R o-rwx ' . Str::after($command, 'sudo mkdir -p');
} }
if (str($command)->contains('$(') || str($command)->contains('`')) { if (str($command)->contains('$(') || str($command)->contains('`')) {
$command = str($command)->replace('$(', '$(sudo ')->replace('`', '`sudo ')->value(); $command = str($command)->replace('$(', '$(sudo ')->replace('`', '`sudo ')->value();
@@ -1397,7 +1403,7 @@ function parseServiceVolumes($serviceVolumes, $resource, $topLevelVolumes, $pull
$isDirectory = data_get($foundConfig, 'is_directory'); $isDirectory = data_get($foundConfig, 'is_directory');
} else { } else {
$isDirectory = (bool) data_get($volume, 'isDirectory', null) || (bool) data_get($volume, 'is_directory', null); $isDirectory = (bool) data_get($volume, 'isDirectory', null) || (bool) data_get($volume, 'is_directory', null);
if ((is_null($isDirectory) || ! $isDirectory) && is_null($content)) { if ((is_null($isDirectory) || !$isDirectory) && is_null($content)) {
// if isDirectory is not set (or false) & content is also not set, we assume it is a directory // if isDirectory is not set (or false) & content is also not set, we assume it is a directory
ray('setting isDirectory to true'); ray('setting isDirectory to true');
$isDirectory = true; $isDirectory = true;
@@ -1412,9 +1418,9 @@ function parseServiceVolumes($serviceVolumes, $resource, $topLevelVolumes, $pull
return $volume; return $volume;
} }
if (get_class($resource) === \App\Models\Application::class) { if (get_class($resource) === \App\Models\Application::class) {
$dir = base_configuration_dir().'/applications/'.$resource->uuid; $dir = base_configuration_dir() . '/applications/' . $resource->uuid;
} else { } else {
$dir = base_configuration_dir().'/services/'.$resource->service->uuid; $dir = base_configuration_dir() . '/services/' . $resource->service->uuid;
} }
if ($source->startsWith('.')) { if ($source->startsWith('.')) {
@@ -1424,9 +1430,9 @@ function parseServiceVolumes($serviceVolumes, $resource, $topLevelVolumes, $pull
$source = $source->replaceFirst('~', $dir); $source = $source->replaceFirst('~', $dir);
} }
if ($pull_request_id !== 0) { if ($pull_request_id !== 0) {
$source = $source."-pr-$pull_request_id"; $source = $source . "-pr-$pull_request_id";
} }
if (! $resource?->settings?->is_preserve_repository_enabled || $foundConfig?->is_based_on_git) { if (!$resource?->settings?->is_preserve_repository_enabled || $foundConfig?->is_based_on_git) {
LocalFileVolume::updateOrCreate( LocalFileVolume::updateOrCreate(
[ [
'mount_path' => $target, 'mount_path' => $target,
@@ -1555,7 +1561,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
if ($serviceLabels->count() > 0) { if ($serviceLabels->count() > 0) {
$removedLabels = collect([]); $removedLabels = collect([]);
$serviceLabels = $serviceLabels->filter(function ($serviceLabel, $serviceLabelName) use ($removedLabels) { $serviceLabels = $serviceLabels->filter(function ($serviceLabel, $serviceLabelName) use ($removedLabels) {
if (! str($serviceLabel)->contains('=')) { if (!str($serviceLabel)->contains('=')) {
$removedLabels->put($serviceLabelName, $serviceLabel); $removedLabels->put($serviceLabelName, $serviceLabel);
return false; return false;
@@ -1637,7 +1643,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$networkExists = $topLevelNetworks->contains(function ($value, $key) use ($networkName) { $networkExists = $topLevelNetworks->contains(function ($value, $key) use ($networkName) {
return $value == $networkName || $key == $networkName; return $value == $networkName || $key == $networkName;
}); });
if (! $networkExists) { if (!$networkExists) {
if (is_string($networkDetails) || is_int($networkDetails)) { if (is_string($networkDetails) || is_int($networkDetails)) {
$topLevelNetworks->put($networkDetails, null); $topLevelNetworks->put($networkDetails, null);
} }
@@ -1663,12 +1669,12 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$savedService->ports = $collectedPorts->implode(','); $savedService->ports = $collectedPorts->implode(',');
$savedService->save(); $savedService->save();
if (! $hasHostNetworkMode) { if (!$hasHostNetworkMode) {
// Add Coolify specific networks // Add Coolify specific networks
$definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) { $definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) {
return $value == $definedNetwork; return $value == $definedNetwork;
}); });
if (! $definedNetworkExists) { if (!$definedNetworkExists) {
foreach ($definedNetwork as $network) { foreach ($definedNetwork as $network) {
$topLevelNetworks->put($network, [ $topLevelNetworks->put($network, [
'name' => $network, 'name' => $network,
@@ -1880,9 +1886,9 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$fqdn = "$fqdn$path"; $fqdn = "$fqdn$path";
} }
if (! $isDatabase) { if (!$isDatabase) {
if ($savedService->fqdn) { if ($savedService->fqdn) {
data_set($savedService, 'fqdn', $savedService->fqdn.','.$fqdn); data_set($savedService, 'fqdn', $savedService->fqdn . ',' . $fqdn);
} else { } else {
data_set($savedService, 'fqdn', $fqdn); data_set($savedService, 'fqdn', $fqdn);
} }
@@ -1897,7 +1903,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
]); ]);
} }
// Caddy needs exact port in some cases. // Caddy needs exact port in some cases.
if ($predefinedPort && ! $key->endsWith("_{$predefinedPort}")) { if ($predefinedPort && !$key->endsWith("_{$predefinedPort}")) {
$fqdns_exploded = str($savedService->fqdn)->explode(','); $fqdns_exploded = str($savedService->fqdn)->explode(',');
if ($fqdns_exploded->count() > 1) { if ($fqdns_exploded->count() > 1) {
continue; continue;
@@ -1937,12 +1943,12 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
'service_id' => $resource->id, 'service_id' => $resource->id,
])->first(); ])->first();
['command' => $command, 'forService' => $forService, 'generatedValue' => $generatedValue, 'port' => $port] = parseEnvVariable($value); ['command' => $command, 'forService' => $forService, 'generatedValue' => $generatedValue, 'port' => $port] = parseEnvVariable($value);
if (! is_null($command)) { if (!is_null($command)) {
if ($command?->value() === 'FQDN' || $command?->value() === 'URL') { if ($command?->value() === 'FQDN' || $command?->value() === 'URL') {
if (Str::lower($forService) === $serviceName) { if (Str::lower($forService) === $serviceName) {
$fqdn = generateFqdn($resource->server, $containerName); $fqdn = generateFqdn($resource->server, $containerName);
} else { } else {
$fqdn = generateFqdn($resource->server, Str::lower($forService).'-'.$resource->uuid); $fqdn = generateFqdn($resource->server, Str::lower($forService) . '-' . $resource->uuid);
} }
if ($port) { if ($port) {
$fqdn = "$fqdn:$port"; $fqdn = "$fqdn:$port";
@@ -1972,13 +1978,13 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
'is_preview' => false, 'is_preview' => false,
]); ]);
} }
if (! $isDatabase) { if (!$isDatabase) {
if ($command->value() === 'FQDN' && is_null($savedService->fqdn) && ! $foundEnv) { if ($command->value() === 'FQDN' && is_null($savedService->fqdn) && !$foundEnv) {
$savedService->fqdn = $fqdn; $savedService->fqdn = $fqdn;
$savedService->save(); $savedService->save();
} }
// Caddy needs exact port in some cases. // Caddy needs exact port in some cases.
if ($predefinedPort && ! $key->endsWith("_{$predefinedPort}") && $command?->value() === 'FQDN' && $resource->server->proxyType() === 'CADDY') { if ($predefinedPort && !$key->endsWith("_{$predefinedPort}") && $command?->value() === 'FQDN' && $resource->server->proxyType() === 'CADDY') {
$fqdns_exploded = str($savedService->fqdn)->explode(','); $fqdns_exploded = str($savedService->fqdn)->explode(',');
if ($fqdns_exploded->count() > 1) { if ($fqdns_exploded->count() > 1) {
continue; continue;
@@ -2000,7 +2006,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
} }
} else { } else {
$generatedValue = generateEnvValue($command, $resource); $generatedValue = generateEnvValue($command, $resource);
if (! $foundEnv) { if (!$foundEnv) {
EnvironmentVariable::create([ EnvironmentVariable::create([
'key' => $key, 'key' => $key,
'value' => $generatedValue, 'value' => $generatedValue,
@@ -2055,7 +2061,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
} }
$defaultLabels = defaultLabels($resource->id, $containerName, type: 'service', subType: $isDatabase ? 'database' : 'application', subId: $savedService->id); $defaultLabels = defaultLabels($resource->id, $containerName, type: 'service', subType: $isDatabase ? 'database' : 'application', subId: $savedService->id);
$serviceLabels = $serviceLabels->merge($defaultLabels); $serviceLabels = $serviceLabels->merge($defaultLabels);
if (! $isDatabase && $fqdns->count() > 0) { if (!$isDatabase && $fqdns->count() > 0) {
if ($fqdns) { if ($fqdns) {
$shouldGenerateLabelsExactly = $resource->server->settings->generate_exact_labels; $shouldGenerateLabelsExactly = $resource->server->settings->generate_exact_labels;
if ($shouldGenerateLabelsExactly) { if ($shouldGenerateLabelsExactly) {
@@ -2123,7 +2129,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
} }
data_set($service, 'labels', $serviceLabels->toArray()); data_set($service, 'labels', $serviceLabels->toArray());
data_forget($service, 'is_database'); data_forget($service, 'is_database');
if (! data_get($service, 'restart')) { if (!data_get($service, 'restart')) {
data_set($service, 'restart', RESTART_MODE); data_set($service, 'restart', RESTART_MODE);
} }
if (data_get($service, 'restart') === 'no' || data_get($service, 'exclude_from_hc')) { if (data_get($service, 'restart') === 'no' || data_get($service, 'exclude_from_hc')) {
@@ -2162,21 +2168,21 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$parsedServiceVariables->put('COOLIFY_CONTAINER_NAME', "$serviceName-{$resource->uuid}"); $parsedServiceVariables->put('COOLIFY_CONTAINER_NAME', "$serviceName-{$resource->uuid}");
// TODO: move this in a shared function // TODO: move this in a shared function
if (! $parsedServiceVariables->has('COOLIFY_APP_NAME')) { if (!$parsedServiceVariables->has('COOLIFY_APP_NAME')) {
$parsedServiceVariables->put('COOLIFY_APP_NAME', "\"{$resource->name}\""); $parsedServiceVariables->put('COOLIFY_APP_NAME', "\"{$resource->name}\"");
} }
if (! $parsedServiceVariables->has('COOLIFY_SERVER_IP')) { if (!$parsedServiceVariables->has('COOLIFY_SERVER_IP')) {
$parsedServiceVariables->put('COOLIFY_SERVER_IP', "\"{$resource->destination->server->ip}\""); $parsedServiceVariables->put('COOLIFY_SERVER_IP', "\"{$resource->destination->server->ip}\"");
} }
if (! $parsedServiceVariables->has('COOLIFY_ENVIRONMENT_NAME')) { if (!$parsedServiceVariables->has('COOLIFY_ENVIRONMENT_NAME')) {
$parsedServiceVariables->put('COOLIFY_ENVIRONMENT_NAME', "\"{$resource->environment->name}\""); $parsedServiceVariables->put('COOLIFY_ENVIRONMENT_NAME', "\"{$resource->environment->name}\"");
} }
if (! $parsedServiceVariables->has('COOLIFY_PROJECT_NAME')) { if (!$parsedServiceVariables->has('COOLIFY_PROJECT_NAME')) {
$parsedServiceVariables->put('COOLIFY_PROJECT_NAME', "\"{$resource->project()->name}\""); $parsedServiceVariables->put('COOLIFY_PROJECT_NAME', "\"{$resource->project()->name}\"");
} }
$parsedServiceVariables = $parsedServiceVariables->map(function ($value, $key) use ($envs_from_coolify) { $parsedServiceVariables = $parsedServiceVariables->map(function ($value, $key) use ($envs_from_coolify) {
if (! str($value)->startsWith('$')) { if (!str($value)->startsWith('$')) {
$found_env = $envs_from_coolify->where('key', $key)->first(); $found_env = $envs_from_coolify->where('key', $key)->first();
if ($found_env) { if ($found_env) {
return $found_env->value; return $found_env->value;
@@ -2260,7 +2266,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
if ($serviceLabels->count() > 0) { if ($serviceLabels->count() > 0) {
$removedLabels = collect([]); $removedLabels = collect([]);
$serviceLabels = $serviceLabels->filter(function ($serviceLabel, $serviceLabelName) use ($removedLabels) { $serviceLabels = $serviceLabels->filter(function ($serviceLabel, $serviceLabelName) use ($removedLabels) {
if (! str($serviceLabel)->contains('=')) { if (!str($serviceLabel)->contains('=')) {
$removedLabels->put($serviceLabelName, $serviceLabel); $removedLabels->put($serviceLabelName, $serviceLabel);
return false; return false;
@@ -2280,11 +2286,11 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$serviceVolumes = $serviceVolumes->map(function ($volume) use ($resource, $topLevelVolumes, $pull_request_id) { $serviceVolumes = $serviceVolumes->map(function ($volume) use ($resource, $topLevelVolumes, $pull_request_id) {
if (is_string($volume)) { if (is_string($volume)) {
$volume = str($volume); $volume = str($volume);
if ($volume->contains(':') && ! $volume->startsWith('/')) { if ($volume->contains(':') && !$volume->startsWith('/')) {
$name = $volume->before(':'); $name = $volume->before(':');
$mount = $volume->after(':'); $mount = $volume->after(':');
if ($name->startsWith('.') || $name->startsWith('~')) { if ($name->startsWith('.') || $name->startsWith('~')) {
$dir = base_configuration_dir().'/applications/'.$resource->uuid; $dir = base_configuration_dir() . '/applications/' . $resource->uuid;
if ($name->startsWith('.')) { if ($name->startsWith('.')) {
$name = $name->replaceFirst('.', $dir); $name = $name->replaceFirst('.', $dir);
} }
@@ -2292,12 +2298,12 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$name = $name->replaceFirst('~', $dir); $name = $name->replaceFirst('~', $dir);
} }
if ($pull_request_id !== 0) { if ($pull_request_id !== 0) {
$name = $name."-pr-$pull_request_id"; $name = $name . "-pr-$pull_request_id";
} }
$volume = str("$name:$mount"); $volume = str("$name:$mount");
} else { } else {
if ($pull_request_id !== 0) { if ($pull_request_id !== 0) {
$name = $name."-pr-$pull_request_id"; $name = $name . "-pr-$pull_request_id";
$volume = str("$name:$mount"); $volume = str("$name:$mount");
if ($topLevelVolumes->has($name)) { if ($topLevelVolumes->has($name)) {
$v = $topLevelVolumes->get($name); $v = $topLevelVolumes->get($name);
@@ -2336,7 +2342,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$name = $volume->before(':'); $name = $volume->before(':');
$mount = $volume->after(':'); $mount = $volume->after(':');
if ($pull_request_id !== 0) { if ($pull_request_id !== 0) {
$name = $name."-pr-$pull_request_id"; $name = $name . "-pr-$pull_request_id";
} }
$volume = str("$name:$mount"); $volume = str("$name:$mount");
} }
@@ -2347,7 +2353,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$read_only = data_get($volume, 'read_only'); $read_only = data_get($volume, 'read_only');
if ($source && $target) { if ($source && $target) {
if ((str($source)->startsWith('.') || str($source)->startsWith('~'))) { if ((str($source)->startsWith('.') || str($source)->startsWith('~'))) {
$dir = base_configuration_dir().'/applications/'.$resource->uuid; $dir = base_configuration_dir() . '/applications/' . $resource->uuid;
if (str($source, '.')) { if (str($source, '.')) {
$source = str($source)->replaceFirst('.', $dir); $source = str($source)->replaceFirst('.', $dir);
} }
@@ -2355,23 +2361,23 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$source = str($source)->replaceFirst('~', $dir); $source = str($source)->replaceFirst('~', $dir);
} }
if ($pull_request_id !== 0) { if ($pull_request_id !== 0) {
$source = $source."-pr-$pull_request_id"; $source = $source . "-pr-$pull_request_id";
} }
if ($read_only) { if ($read_only) {
data_set($volume, 'source', $source.':'.$target.':ro'); data_set($volume, 'source', $source . ':' . $target . ':ro');
} else { } else {
data_set($volume, 'source', $source.':'.$target); data_set($volume, 'source', $source . ':' . $target);
} }
} else { } else {
if ($pull_request_id !== 0) { if ($pull_request_id !== 0) {
$source = $source."-pr-$pull_request_id"; $source = $source . "-pr-$pull_request_id";
} }
if ($read_only) { if ($read_only) {
data_set($volume, 'source', $source.':'.$target.':ro'); data_set($volume, 'source', $source . ':' . $target . ':ro');
} else { } else {
data_set($volume, 'source', $source.':'.$target); data_set($volume, 'source', $source . ':' . $target);
} }
if (! str($source)->startsWith('/')) { if (!str($source)->startsWith('/')) {
if ($topLevelVolumes->has($source)) { if ($topLevelVolumes->has($source)) {
$v = $topLevelVolumes->get($source); $v = $topLevelVolumes->get($source);
if (data_get($v, 'driver_opts.type') === 'cifs') { if (data_get($v, 'driver_opts.type') === 'cifs') {
@@ -2404,11 +2410,11 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$serviceVolumes = $serviceVolumes->map(function ($volume) use ($resource, $topLevelVolumes, $pull_request_id) { $serviceVolumes = $serviceVolumes->map(function ($volume) use ($resource, $topLevelVolumes, $pull_request_id) {
if (is_string($volume)) { if (is_string($volume)) {
$volume = str($volume); $volume = str($volume);
if ($volume->contains(':') && ! $volume->startsWith('/')) { if ($volume->contains(':') && !$volume->startsWith('/')) {
$name = $volume->before(':'); $name = $volume->before(':');
$mount = $volume->after(':'); $mount = $volume->after(':');
if ($name->startsWith('.') || $name->startsWith('~')) { if ($name->startsWith('.') || $name->startsWith('~')) {
$dir = base_configuration_dir().'/applications/'.$resource->uuid; $dir = base_configuration_dir() . '/applications/' . $resource->uuid;
if ($name->startsWith('.')) { if ($name->startsWith('.')) {
$name = $name->replaceFirst('.', $dir); $name = $name->replaceFirst('.', $dir);
} }
@@ -2416,13 +2422,13 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$name = $name->replaceFirst('~', $dir); $name = $name->replaceFirst('~', $dir);
} }
if ($pull_request_id !== 0) { if ($pull_request_id !== 0) {
$name = $name."-pr-$pull_request_id"; $name = $name . "-pr-$pull_request_id";
} }
$volume = str("$name:$mount"); $volume = str("$name:$mount");
} else { } else {
if ($pull_request_id !== 0) { if ($pull_request_id !== 0) {
$uuid = $resource->uuid; $uuid = $resource->uuid;
$name = $uuid."-$name-pr-$pull_request_id"; $name = $uuid . "-$name-pr-$pull_request_id";
$volume = str("$name:$mount"); $volume = str("$name:$mount");
if ($topLevelVolumes->has($name)) { if ($topLevelVolumes->has($name)) {
$v = $topLevelVolumes->get($name); $v = $topLevelVolumes->get($name);
@@ -2441,7 +2447,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
} }
} else { } else {
$uuid = $resource->uuid; $uuid = $resource->uuid;
$name = str($uuid."-$name"); $name = str($uuid . "-$name");
$volume = str("$name:$mount"); $volume = str("$name:$mount");
if ($topLevelVolumes->has($name->value())) { if ($topLevelVolumes->has($name->value())) {
$v = $topLevelVolumes->get($name->value()); $v = $topLevelVolumes->get($name->value());
@@ -2464,7 +2470,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$name = $volume->before(':'); $name = $volume->before(':');
$mount = $volume->after(':'); $mount = $volume->after(':');
if ($pull_request_id !== 0) { if ($pull_request_id !== 0) {
$name = $name."-pr-$pull_request_id"; $name = $name . "-pr-$pull_request_id";
} }
$volume = str("$name:$mount"); $volume = str("$name:$mount");
} }
@@ -2476,7 +2482,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
if ($source && $target) { if ($source && $target) {
$uuid = $resource->uuid; $uuid = $resource->uuid;
if ((str($source)->startsWith('.') || str($source)->startsWith('~') || str($source)->startsWith('/'))) { if ((str($source)->startsWith('.') || str($source)->startsWith('~') || str($source)->startsWith('/'))) {
$dir = base_configuration_dir().'/applications/'.$resource->uuid; $dir = base_configuration_dir() . '/applications/' . $resource->uuid;
if (str($source, '.')) { if (str($source, '.')) {
$source = str($source)->replaceFirst('.', $dir); $source = str($source)->replaceFirst('.', $dir);
} }
@@ -2484,22 +2490,22 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$source = str($source)->replaceFirst('~', $dir); $source = str($source)->replaceFirst('~', $dir);
} }
if ($read_only) { if ($read_only) {
data_set($volume, 'source', $source.':'.$target.':ro'); data_set($volume, 'source', $source . ':' . $target . ':ro');
} else { } else {
data_set($volume, 'source', $source.':'.$target); data_set($volume, 'source', $source . ':' . $target);
} }
} else { } else {
if ($pull_request_id === 0) { if ($pull_request_id === 0) {
$source = $uuid."-$source"; $source = $uuid . "-$source";
} else { } else {
$source = $uuid."-$source-pr-$pull_request_id"; $source = $uuid . "-$source-pr-$pull_request_id";
} }
if ($read_only) { if ($read_only) {
data_set($volume, 'source', $source.':'.$target.':ro'); data_set($volume, 'source', $source . ':' . $target . ':ro');
} else { } else {
data_set($volume, 'source', $source.':'.$target); data_set($volume, 'source', $source . ':' . $target);
} }
if (! str($source)->startsWith('/')) { if (!str($source)->startsWith('/')) {
if ($topLevelVolumes->has($source)) { if ($topLevelVolumes->has($source)) {
$v = $topLevelVolumes->get($source); $v = $topLevelVolumes->get($source);
if (data_get($v, 'driver_opts.type') === 'cifs') { if (data_get($v, 'driver_opts.type') === 'cifs') {
@@ -2532,7 +2538,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
if ($pull_request_id !== 0 && count($serviceDependencies) > 0) { if ($pull_request_id !== 0 && count($serviceDependencies) > 0) {
$serviceDependencies = $serviceDependencies->map(function ($dependency) use ($pull_request_id) { $serviceDependencies = $serviceDependencies->map(function ($dependency) use ($pull_request_id) {
return $dependency."-pr-$pull_request_id"; return $dependency . "-pr-$pull_request_id";
}); });
data_set($service, 'depends_on', $serviceDependencies->toArray()); data_set($service, 'depends_on', $serviceDependencies->toArray());
} }
@@ -2554,7 +2560,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$networkExists = $topLevelNetworks->contains(function ($value, $key) use ($networkName) { $networkExists = $topLevelNetworks->contains(function ($value, $key) use ($networkName) {
return $value == $networkName || $key == $networkName; return $value == $networkName || $key == $networkName;
}); });
if (! $networkExists) { if (!$networkExists) {
if (is_string($networkDetails) || is_int($networkDetails)) { if (is_string($networkDetails) || is_int($networkDetails)) {
$topLevelNetworks->put($networkDetails, null); $topLevelNetworks->put($networkDetails, null);
} }
@@ -2582,7 +2588,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) { $definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) {
return $value == $definedNetwork; return $value == $definedNetwork;
}); });
if (! $definedNetworkExists) { if (!$definedNetworkExists) {
foreach ($definedNetwork as $network) { foreach ($definedNetwork as $network) {
if ($pull_request_id !== 0) { if ($pull_request_id !== 0) {
$topLevelNetworks->put($network, [ $topLevelNetworks->put($network, [
@@ -2700,12 +2706,12 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
'application_id' => $resource->id, 'application_id' => $resource->id,
])->first(); ])->first();
['command' => $command, 'forService' => $forService, 'generatedValue' => $generatedValue, 'port' => $port] = parseEnvVariable($value); ['command' => $command, 'forService' => $forService, 'generatedValue' => $generatedValue, 'port' => $port] = parseEnvVariable($value);
if (! is_null($command)) { if (!is_null($command)) {
if ($command?->value() === 'FQDN' || $command?->value() === 'URL') { if ($command?->value() === 'FQDN' || $command?->value() === 'URL') {
if (Str::lower($forService) === $serviceName) { if (Str::lower($forService) === $serviceName) {
$fqdn = generateFqdn($server, $containerName); $fqdn = generateFqdn($server, $containerName);
} else { } else {
$fqdn = generateFqdn($server, Str::lower($forService).'-'.$resource->uuid); $fqdn = generateFqdn($server, Str::lower($forService) . '-' . $resource->uuid);
} }
if ($port) { if ($port) {
$fqdn = "$fqdn:$port"; $fqdn = "$fqdn:$port";
@@ -2726,7 +2732,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
} }
} else { } else {
$generatedValue = generateEnvValue($command); $generatedValue = generateEnvValue($command);
if (! $foundEnv) { if (!$foundEnv) {
EnvironmentVariable::create([ EnvironmentVariable::create([
'key' => $key, 'key' => $key,
'value' => $generatedValue, 'value' => $generatedValue,
@@ -2898,7 +2904,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
} }
data_set($service, 'labels', $serviceLabels->toArray()); data_set($service, 'labels', $serviceLabels->toArray());
data_forget($service, 'is_database'); data_forget($service, 'is_database');
if (! data_get($service, 'restart')) { if (!data_get($service, 'restart')) {
data_set($service, 'restart', RESTART_MODE); data_set($service, 'restart', RESTART_MODE);
} }
data_set($service, 'container_name', $containerName); data_set($service, 'container_name', $containerName);
@@ -2909,7 +2915,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
}); });
if ($pull_request_id !== 0) { if ($pull_request_id !== 0) {
$services->each(function ($service, $serviceName) use ($pull_request_id, $services) { $services->each(function ($service, $serviceName) use ($pull_request_id, $services) {
$services[$serviceName."-pr-$pull_request_id"] = $service; $services[$serviceName . "-pr-$pull_request_id"] = $service;
data_forget($services, $serviceName); data_forget($services, $serviceName);
}); });
} }
@@ -2937,7 +2943,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$uuid = data_get($resource, 'uuid'); $uuid = data_get($resource, 'uuid');
$compose = data_get($resource, 'docker_compose_raw'); $compose = data_get($resource, 'docker_compose_raw');
if (! $compose) { if (!$compose) {
return collect([]); return collect([]);
} }
@@ -3330,29 +3336,29 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$isDirectory = data_get($foundConfig, 'is_directory'); $isDirectory = data_get($foundConfig, 'is_directory');
} else { } else {
// if isDirectory is not set (or false) & content is also not set, we assume it is a directory // if isDirectory is not set (or false) & content is also not set, we assume it is a directory
if ((is_null($isDirectory) || ! $isDirectory) && is_null($content)) { if ((is_null($isDirectory) || !$isDirectory) && is_null($content)) {
$isDirectory = true; $isDirectory = true;
} }
} }
} }
if ($type->value() === 'bind') { if ($type->value() === 'bind') {
if ($source->value() === '/var/run/docker.sock') { if ($source->value() === '/var/run/docker.sock') {
$volume = $source->value().':'.$target->value(); $volume = $source->value() . ':' . $target->value();
} elseif ($source->value() === '/tmp' || $source->value() === '/tmp/') { } elseif ($source->value() === '/tmp' || $source->value() === '/tmp/') {
$volume = $source->value().':'.$target->value(); $volume = $source->value() . ':' . $target->value();
} else { } else {
if ((int) $resource->compose_parsing_version >= 4) { if ((int) $resource->compose_parsing_version >= 4) {
if ($isApplication) { if ($isApplication) {
$mainDirectory = str(base_configuration_dir().'/applications/'.$uuid); $mainDirectory = str(base_configuration_dir() . '/applications/' . $uuid);
} elseif ($isService) { } elseif ($isService) {
$mainDirectory = str(base_configuration_dir().'/services/'.$uuid); $mainDirectory = str(base_configuration_dir() . '/services/' . $uuid);
} }
} else { } else {
$mainDirectory = str(base_configuration_dir().'/applications/'.$uuid); $mainDirectory = str(base_configuration_dir() . '/applications/' . $uuid);
} }
$source = replaceLocalSource($source, $mainDirectory); $source = replaceLocalSource($source, $mainDirectory);
if ($isApplication && $isPullRequest) { if ($isApplication && $isPullRequest) {
$source = $source."-pr-$pullRequestId"; $source = $source . "-pr-$pullRequestId";
} }
LocalFileVolume::updateOrCreate( LocalFileVolume::updateOrCreate(
[ [
@@ -3372,12 +3378,12 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
if (isDev()) { if (isDev()) {
if ((int) $resource->compose_parsing_version >= 4) { if ((int) $resource->compose_parsing_version >= 4) {
if ($isApplication) { if ($isApplication) {
$source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/applications/'.$uuid); $source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/applications/' . $uuid);
} elseif ($isService) { } elseif ($isService) {
$source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/services/'.$uuid); $source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/services/' . $uuid);
} }
} else { } else {
$source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/applications/'.$uuid); $source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/applications/' . $uuid);
} }
} }
$volume = "$source:$target"; $volume = "$source:$target";
@@ -3444,7 +3450,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$depends_on = $newDependsOn; $depends_on = $newDependsOn;
} }
} }
if (! $use_network_mode) { if (!$use_network_mode) {
if ($topLevel->get('networks')?->count() > 0) { if ($topLevel->get('networks')?->count() > 0) {
foreach ($topLevel->get('networks') as $networkName => $network) { foreach ($topLevel->get('networks') as $networkName => $network) {
if ($networkName === 'default') { if ($networkName === 'default') {
@@ -3457,7 +3463,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$networkExists = $networks->contains(function ($value, $key) use ($networkName) { $networkExists = $networks->contains(function ($value, $key) use ($networkName) {
return $value == $networkName || $key == $networkName; return $value == $networkName || $key == $networkName;
}); });
if (! $networkExists) { if (!$networkExists) {
$networks->put($networkName, null); $networks->put($networkName, null);
} }
} }
@@ -3465,7 +3471,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$baseNetworkExists = $networks->contains(function ($value, $_) use ($baseNetwork) { $baseNetworkExists = $networks->contains(function ($value, $_) use ($baseNetwork) {
return $value == $baseNetwork; return $value == $baseNetwork;
}); });
if (! $baseNetworkExists) { if (!$baseNetworkExists) {
foreach ($baseNetwork as $network) { foreach ($baseNetwork as $network) {
$topLevel->get('networks')->put($network, [ $topLevel->get('networks')->put($network, [
'name' => $network, 'name' => $network,
@@ -3497,7 +3503,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$networks_temp = collect(); $networks_temp = collect();
if (! $use_network_mode) { if (!$use_network_mode) {
foreach ($networks as $key => $network) { foreach ($networks as $key => $network) {
if (gettype($network) === 'string') { if (gettype($network) === 'string') {
// networks: // networks:
@@ -3528,7 +3534,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$normalEnvironments = $environment->diffKeys($allMagicEnvironments); $normalEnvironments = $environment->diffKeys($allMagicEnvironments);
$normalEnvironments = $normalEnvironments->filter(function ($value, $key) { $normalEnvironments = $normalEnvironments->filter(function ($value, $key) {
return ! str($value)->startsWith('SERVICE_'); return !str($value)->startsWith('SERVICE_');
}); });
foreach ($normalEnvironments as $key => $value) { foreach ($normalEnvironments as $key => $value) {
@@ -3548,7 +3554,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
continue; continue;
} }
if (! $value->startsWith('$')) { if (!$value->startsWith('$')) {
continue; continue;
} }
if ($key->value() === $parsedValue->value()) { if ($key->value() === $parsedValue->value()) {
@@ -3679,7 +3685,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$defaultLabels = defaultLabels($resource->id, $containerName, type: 'service', subType: $isDatabase ? 'database' : 'application', subId: $savedService->id); $defaultLabels = defaultLabels($resource->id, $containerName, type: 'service', subType: $isDatabase ? 'database' : 'application', subId: $savedService->id);
} }
// Add COOLIFY_FQDN & COOLIFY_URL to environment // Add COOLIFY_FQDN & COOLIFY_URL to environment
if (! $isDatabase && $fqdns instanceof Collection && $fqdns->count() > 0) { if (!$isDatabase && $fqdns instanceof Collection && $fqdns->count() > 0) {
$coolifyEnvironments->put('COOLIFY_URL', $fqdns->implode(',')); $coolifyEnvironments->put('COOLIFY_URL', $fqdns->implode(','));
$urls = $fqdns->map(function ($fqdn) { $urls = $fqdns->map(function ($fqdn) {
@@ -3691,7 +3697,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
if ($environment->count() > 0) { if ($environment->count() > 0) {
$environment = $environment->filter(function ($value, $key) { $environment = $environment->filter(function ($value, $key) {
return ! str($key)->startsWith('SERVICE_FQDN_'); return !str($key)->startsWith('SERVICE_FQDN_');
})->map(function ($value, $key) use ($resource) { })->map(function ($value, $key) use ($resource) {
// if value is empty, set it to null so if you set the environment variable in the .env file (Coolify's UI), it will used // if value is empty, set it to null so if you set the environment variable in the .env file (Coolify's UI), it will used
if (str($value)->isEmpty()) { if (str($value)->isEmpty()) {
@@ -3718,7 +3724,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
}); });
} }
} }
if (! $isDatabase && $fqdns instanceof Collection && $fqdns->count() > 0) { if (!$isDatabase && $fqdns instanceof Collection && $fqdns->count() > 0) {
if ($isApplication) { if ($isApplication) {
$shouldGenerateLabelsExactly = $resource->destination->server->settings->generate_exact_labels; $shouldGenerateLabelsExactly = $resource->destination->server->settings->generate_exact_labels;
$uuid = $resource->uuid; $uuid = $resource->uuid;
@@ -3811,7 +3817,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
'restart' => $restart->value(), 'restart' => $restart->value(),
'labels' => $serviceLabels, 'labels' => $serviceLabels,
]); ]);
if (! $use_network_mode) { if (!$use_network_mode) {
$payload['networks'] = $networks_temp; $payload['networks'] = $networks_temp;
} }
if ($ports->count() > 0) { if ($ports->count() > 0) {
@@ -3871,7 +3877,7 @@ function isAssociativeArray($array)
$array = $array->toArray(); $array = $array->toArray();
} }
if (! is_array($array)) { if (!is_array($array)) {
throw new \InvalidArgumentException('Input must be an array or a Collection.'); throw new \InvalidArgumentException('Input must be an array or a Collection.');
} }
@@ -3984,7 +3990,7 @@ function instanceSettings()
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(); $server = Server::find($server_id)->where('team_id', $team_id)->first();
if (! $server) { if (!$server) {
return; return;
} }
$uuid = new Cuid2; $uuid = new Cuid2;
@@ -4011,7 +4017,7 @@ function loadConfigFromGit(string $repository, string $branch, string $base_dire
function loggy($message = null, array $context = []) function loggy($message = null, array $context = [])
{ {
if (! isDev()) { if (!isDev()) {
return; return;
} }
if (function_exists('ray') && config('app.debug')) { if (function_exists('ray') && config('app.debug')) {
@@ -4046,7 +4052,7 @@ function isEmailRateLimited(string $limiterKey, int $decaySeconds = 3600, ?calla
$limiterKey, $limiterKey,
$maxAttempts = 0, $maxAttempts = 0,
function () use (&$rateLimited, &$limiterKey, $callbackOnSuccess) { function () use (&$rateLimited, &$limiterKey, $callbackOnSuccess) {
isDev() && loggy('Rate limit not reached for '.$limiterKey); isDev() && loggy('Rate limit not reached for ' . $limiterKey);
$rateLimited = false; $rateLimited = false;
if ($callbackOnSuccess) { if ($callbackOnSuccess) {
@@ -4055,8 +4061,8 @@ function isEmailRateLimited(string $limiterKey, int $decaySeconds = 3600, ?calla
}, },
$decaySeconds, $decaySeconds,
); );
if (! $executed) { if (!$executed) {
isDev() && loggy('Rate limit reached for '.$limiterKey.'. Rate limiter will be disabled for '.$decaySeconds.' seconds.'); isDev() && loggy('Rate limit reached for ' . $limiterKey . '. Rate limiter will be disabled for ' . $decaySeconds . ' seconds.');
$rateLimited = true; $rateLimited = true;
} }

View File

@@ -0,0 +1,37 @@
<?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->boolean('slack_enabled')->default(false);
$table->string('slack_webhook_url')->nullable();
$table->boolean('slack_notifications_test')->default(false);
$table->boolean('slack_notifications_deployments')->default(false);
$table->boolean('slack_notifications_status_changes')->default(false);
$table->boolean('slack_notifications_database_backups')->default(false);
$table->boolean('slack_notifications_scheduled_tasks')->default(false);
$table->boolean('slack_notifications_server_disk_usage')->default(false);
});
}
public function down(): void
{
Schema::table('teams', function (Blueprint $table) {
$table->dropColumn([
'slack_enabled',
'slack_webhook_url',
'slack_notifications_test',
'slack_notifications_deployments',
'slack_notifications_status_changes',
'slack_notifications_database_backups',
'slack_notifications_scheduled_tasks',
'slack_notifications_server_disk_usage',
]);
});
}
};

View File

@@ -15,6 +15,10 @@
href="{{ route('notifications.discord') }}"> href="{{ route('notifications.discord') }}">
<button>Discord</button> <button>Discord</button>
</a> </a>
<a class="{{ request()->routeIs('notifications.slack') ? 'dark:text-white' : '' }}"
href="{{ route('notifications.slack') }}">
<button>Slack</button>
</a>
</nav> </nav>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,43 @@
<div>
<x-slot:title>
Notifications | Coolify
</x-slot>
<x-notification.navbar />
<form wire:submit='submit' class="flex flex-col gap-4">
<div class="flex items-center gap-2">
<h2>Slack</h2>
<x-forms.button type="submit">
Save
</x-forms.button>
@if ($slackEnabled)
<x-forms.button class="normal-case dark:text-white btn btn-xs no-animation btn-primary"
wire:click="sendTestNotification">
Send Test Notifications
</x-forms.button>
@endif
</div>
<div class="w-32">
<x-forms.checkbox instantSave="instantSaveSlackEnabled" id="slackEnabled" label="Enabled" />
</div>
<x-forms.input type="password"
helper="Generate a webhook in Slack.<br>Example: https://hooks.slack.com/services/...." required
id="slackWebhookUrl" label="Webhook" />
</form>
@if ($slackEnabled)
<h2 class="mt-4">Subscribe to events</h2>
<div class="w-64">
@if (isDev())
<x-forms.checkbox instantSave="saveModel" id="slackNotificationsTest" label="Test" />
@endif
<x-forms.checkbox instantSave="saveModel" id="slackNotificationsStatusChanges"
label="Container Status Changes" />
<x-forms.checkbox instantSave="saveModel" id="slackNotificationsDeployments"
label="Application Deployments" />
<x-forms.checkbox instantSave="saveModel" id="slackNotificationsDatabaseBackups" label="Backup Status" />
<x-forms.checkbox instantSave="saveModel" id="slackNotificationsScheduledTasks"
label="Scheduled Tasks Status" />
<x-forms.checkbox instantSave="saveModel" id="slackNotificationsServerDiskUsage"
label="Server Disk Usage" />
</div>
@endif
</div>

View File

@@ -133,6 +133,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/email', NotificationEmail::class)->name('notifications.email'); Route::get('/email', NotificationEmail::class)->name('notifications.email');
Route::get('/telegram', NotificationTelegram::class)->name('notifications.telegram'); Route::get('/telegram', NotificationTelegram::class)->name('notifications.telegram');
Route::get('/discord', NotificationDiscord::class)->name('notifications.discord'); Route::get('/discord', NotificationDiscord::class)->name('notifications.discord');
Route::get('/slack', App\Livewire\Notifications\Slack::class)->name('notifications.slack');
}); });
Route::prefix('storages')->group(function () { Route::prefix('storages')->group(function () {