diff --git a/app/Actions/Proxy/StartProxy.php b/app/Actions/Proxy/StartProxy.php index 7c93720cb..9bc506d9b 100644 --- a/app/Actions/Proxy/StartProxy.php +++ b/app/Actions/Proxy/StartProxy.php @@ -2,6 +2,7 @@ namespace App\Actions\Proxy; +use App\Enums\ProxyTypes; use App\Events\ProxyStarted; use App\Models\Server; use Lorisleiva\Actions\Concerns\AsAction; @@ -37,11 +38,16 @@ class StartProxy "echo 'Successfully started coolify-proxy.'", ]); } else { - $caddfile = 'import /dynamic/*.caddy'; + if (isDev()) { + if ($proxyType === ProxyTypes::CADDY->value) { + $proxy_path = '/data/coolify/proxy/caddy'; + } + } + $caddyfile = 'import /dynamic/*.caddy'; $commands = $commands->merge([ "mkdir -p $proxy_path/dynamic", "cd $proxy_path", - "echo '$caddfile' > $proxy_path/dynamic/Caddyfile", + "echo '$caddyfile' > $proxy_path/dynamic/Caddyfile", "echo 'Creating required Docker Compose file.'", "echo 'Pulling docker image.'", 'docker compose pull', diff --git a/app/Jobs/SendMessageToSlackJob.php b/app/Jobs/SendMessageToSlackJob.php new file mode 100644 index 000000000..470002d23 --- /dev/null +++ b/app/Jobs/SendMessageToSlackJob.php @@ -0,0 +1,59 @@ +onQueue('high'); + } + + 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, + ], + ], + ], + ], + ], + ]); + } +} diff --git a/app/Listeners/ProxyStartedNotification.php b/app/Listeners/ProxyStartedNotification.php index d0541b162..9045b1e5c 100644 --- a/app/Listeners/ProxyStartedNotification.php +++ b/app/Listeners/ProxyStartedNotification.php @@ -14,7 +14,7 @@ class ProxyStartedNotification public function handle(ProxyStarted $event): void { $this->server = data_get($event, 'data'); - $this->server->setupDefault404Redirect(); + $this->server->setupDefaultRedirect(); $this->server->setupDynamicProxyConfiguration(); $this->server->proxy->force_stop = false; $this->server->save(); diff --git a/app/Livewire/Notifications/Slack.php b/app/Livewire/Notifications/Slack.php new file mode 100644 index 000000000..edd32a071 --- /dev/null +++ b/app/Livewire/Notifications/Slack.php @@ -0,0 +1,130 @@ +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'); + } +} \ No newline at end of file diff --git a/app/Livewire/Server/Proxy.php b/app/Livewire/Server/Proxy.php index 0b069ddb9..4e325c1ff 100644 --- a/app/Livewire/Server/Proxy.php +++ b/app/Livewire/Server/Proxy.php @@ -15,6 +15,8 @@ class Proxy extends Component public $proxy_settings = null; + public bool $redirect_enabled = true; + public ?string $redirect_url = null; protected $listeners = ['proxyStatusUpdated', 'saveConfiguration' => 'submit']; @@ -26,6 +28,7 @@ class Proxy extends Component public function mount() { $this->selectedProxy = $this->server->proxyType(); + $this->redirect_enabled = data_get($this->server, 'proxy.redirect_enabled', true); $this->redirect_url = data_get($this->server, 'proxy.redirect_url'); } @@ -38,7 +41,7 @@ class Proxy extends Component { $this->server->proxy = null; $this->server->save(); - $this->dispatch('proxyChanged'); + $this->dispatch('reloadWindow'); } public function selectProxy($proxy_type) @@ -46,7 +49,7 @@ class Proxy extends Component try { $this->server->changeProxy($proxy_type, async: false); $this->selectedProxy = $this->server->proxy->type; - $this->dispatch('proxyStatusUpdated'); + $this->dispatch('reloadWindow'); } catch (\Throwable $e) { return handleError($e, $this); } @@ -63,13 +66,25 @@ class Proxy extends Component } } + public function instantSaveRedirect() + { + try { + $this->server->proxy->redirect_enabled = $this->redirect_enabled; + $this->server->save(); + $this->server->setupDefaultRedirect(); + $this->dispatch('success', 'Proxy configuration saved.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + public function submit() { try { SaveConfiguration::run($this->server, $this->proxy_settings); $this->server->proxy->redirect_url = $this->redirect_url; $this->server->save(); - $this->server->setupDefault404Redirect(); + $this->server->setupDefaultRedirect(); $this->dispatch('success', 'Proxy configuration saved.'); } catch (\Throwable $e) { return handleError($e, $this); diff --git a/app/Livewire/Server/Proxy/Deploy.php b/app/Livewire/Server/Proxy/Deploy.php index 8fcff85d6..4f9d41092 100644 --- a/app/Livewire/Server/Proxy/Deploy.php +++ b/app/Livewire/Server/Proxy/Deploy.php @@ -65,7 +65,7 @@ class Deploy extends Component public function restart() { try { - $this->stop(forceStop: false); + $this->stop(); $this->dispatch('checkProxy'); } catch (\Throwable $e) { return handleError($e, $this); @@ -105,6 +105,7 @@ class Deploy extends Component $startTime = Carbon::now()->getTimestamp(); while ($process->running()) { + ray('running'); if (Carbon::now()->getTimestamp() - $startTime >= $timeout) { $this->forceStopContainer($containerName); break; diff --git a/app/Models/Application.php b/app/Models/Application.php index a68c1d54a..d1efd3f33 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -1321,17 +1321,43 @@ class Application extends BaseModel if (! $gitRemoteStatus['is_accessible']) { throw new \RuntimeException("Failed to read Git source:\n\n{$gitRemoteStatus['error']}"); } + $getGitVersion = instant_remote_process(['git --version'], $this->destination->server, false); + $gitVersion = str($getGitVersion)->explode(' ')->last(); - $commands = collect([ - "rm -rf /tmp/{$uuid}", - "mkdir -p /tmp/{$uuid}", - "cd /tmp/{$uuid}", - $cloneCommand, - 'git sparse-checkout init --cone', - "git sparse-checkout set {$fileList->implode(' ')}", - 'git read-tree -mu HEAD', - "cat .$workdir$composeFile", - ]); + if (version_compare($gitVersion, '2.35.1', '<')) { + $fileList = $fileList->map(function ($file) { + $parts = explode('/', trim($file, '.')); + $paths = collect(); + $currentPath = ''; + foreach ($parts as $part) { + $currentPath .= ($currentPath ? '/' : '').$part; + $paths->push($currentPath); + } + + return $paths; + })->flatten()->unique()->values(); + $commands = collect([ + "rm -rf /tmp/{$uuid}", + "mkdir -p /tmp/{$uuid}", + "cd /tmp/{$uuid}", + $cloneCommand, + 'git sparse-checkout init --cone', + "git sparse-checkout set {$fileList->implode(' ')}", + 'git read-tree -mu HEAD', + "cat .$workdir$composeFile", + ]); + } else { + $commands = collect([ + "rm -rf /tmp/{$uuid}", + "mkdir -p /tmp/{$uuid}", + "cd /tmp/{$uuid}", + $cloneCommand, + 'git sparse-checkout init --cone', + "git sparse-checkout set {$fileList->implode(' ')}", + 'git read-tree -mu HEAD', + "cat .$workdir$composeFile", + ]); + } try { $composeFileContent = instant_remote_process($commands, $this->destination->server); } catch (\Exception $e) { diff --git a/app/Models/Server.php b/app/Models/Server.php index 77673b959..6dfb0a4a1 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -105,6 +105,14 @@ class Server extends BaseModel ]); } } + if (! isset($server->proxy->redirect_enabled)) { + $server->proxy->redirect_enabled = true; + } + }); + static::retrieved(function ($server) { + if (! isset($server->proxy->redirect_enabled)) { + $server->proxy->redirect_enabled = true; + } }); static::forceDeleting(function ($server) { @@ -184,73 +192,80 @@ class Server extends BaseModel return $this->proxyType() && $this->proxyType() !== 'NONE' && $this->isFunctional() && ! $this->isSwarmWorker() && ! $this->settings->is_build_server; } - public function setupDefault404Redirect() + public function setupDefaultRedirect() { + $banner = + "# This file is generated by Coolify, do not edit it manually.\n". + "# Disable the default redirect to customize (only if you know what are you doing).\n\n"; $dynamic_conf_path = $this->proxyPath().'/dynamic'; $proxy_type = $this->proxyType(); + $redirect_enabled = $this->proxy->redirect_enabled ?? true; $redirect_url = $this->proxy->redirect_url; - if ($proxy_type === ProxyTypes::TRAEFIK->value) { - $default_redirect_file = "$dynamic_conf_path/default_redirect_404.yaml"; - } elseif ($proxy_type === ProxyTypes::CADDY->value) { - $default_redirect_file = "$dynamic_conf_path/default_redirect_404.caddy"; - } - if (empty($redirect_url)) { + if (isDev()) { if ($proxy_type === ProxyTypes::CADDY->value) { - $conf = ':80, :443 { -respond 404 -}'; - $conf = - "# This file is automatically generated by Coolify.\n". - "# Do not edit it manually (only if you know what are you doing).\n\n". - $conf; - $base64 = base64_encode($conf); - instant_remote_process([ - "mkdir -p $dynamic_conf_path", - "echo '$base64' | base64 -d | tee $default_redirect_file > /dev/null", - ], $this); - $this->reloadCaddy(); - - return; + $dynamic_conf_path = '/data/coolify/proxy/caddy/dynamic'; } - instant_remote_process([ - "mkdir -p $dynamic_conf_path", - "rm -f $default_redirect_file", - ], $this); - - return; } if ($proxy_type === ProxyTypes::TRAEFIK->value) { - $dynamic_conf = [ - 'http' => [ - 'routers' => [ - 'catchall' => [ - 'entryPoints' => [ - 0 => 'http', - 1 => 'https', - ], - 'service' => 'noop', - 'rule' => 'HostRegexp(`.+`)', - 'tls' => [ - 'certResolver' => 'letsencrypt', - ], - 'priority' => 1, - 'middlewares' => [ - 0 => 'redirect-regexp', + $default_redirect_file = "$dynamic_conf_path/default_redirect_503.yaml"; + } elseif ($proxy_type === ProxyTypes::CADDY->value) { + $default_redirect_file = "$dynamic_conf_path/default_redirect_503.caddy"; + } + + instant_remote_process([ + "mkdir -p $dynamic_conf_path", + "rm -f $dynamic_conf_path/default_redirect_404.yaml", + "rm -f $dynamic_conf_path/default_redirect_404.caddy", + ], $this); + + if ($redirect_enabled === false) { + instant_remote_process(["rm -f $default_redirect_file"], $this); + } else { + if ($proxy_type === ProxyTypes::CADDY->value) { + if (filled($redirect_url)) { + $conf = ":80, :443 { + redir $redirect_url +}"; + } else { + $conf = ':80, :443 { + respond 503 +}'; + } + } elseif ($proxy_type === ProxyTypes::TRAEFIK->value) { + $dynamic_conf = [ + 'http' => [ + 'routers' => [ + 'catchall' => [ + 'entryPoints' => [ + 0 => 'http', + 1 => 'https', + ], + 'service' => 'noop', + 'rule' => 'PathPrefix(`/`)', + 'tls' => [ + 'certResolver' => 'letsencrypt', + ], + 'priority' => -1000, ], ], - ], - 'services' => [ - 'noop' => [ - 'loadBalancer' => [ - 'servers' => [ - 0 => [ - 'url' => '', - ], + 'services' => [ + 'noop' => [ + 'loadBalancer' => [ + 'servers' => [], ], ], ], ], - 'middlewares' => [ + ]; + if (filled($redirect_url)) { + $dynamic_conf['http']['routers']['catchall']['middlewares'] = [ + 0 => 'redirect-regexp', + ]; + + $dynamic_conf['http']['services']['noop']['loadBalancer']['servers'][0] = [ + 'url' => '', + ]; + $dynamic_conf['http']['middlewares'] = [ 'redirect-regexp' => [ 'redirectRegex' => [ 'regex' => '(.*)', @@ -258,32 +273,17 @@ respond 404 'permanent' => false, ], ], - ], - ], - ]; - $conf = Yaml::dump($dynamic_conf, 12, 2); - $conf = - "# This file is automatically generated by Coolify.\n". - "# Do not edit it manually (only if you know what are you doing).\n\n". - $conf; - - $base64 = base64_encode($conf); - } elseif ($proxy_type === ProxyTypes::CADDY->value) { - $conf = ":80, :443 { - redir $redirect_url -}"; - $conf = - "# This file is automatically generated by Coolify.\n". - "# Do not edit it manually (only if you know what are you doing).\n\n". - $conf; + ]; + } + $conf = Yaml::dump($dynamic_conf, 12, 2); + } + $conf = $banner.$conf; $base64 = base64_encode($conf); + instant_remote_process([ + "echo '$base64' | base64 -d | tee $default_redirect_file > /dev/null", + ], $this); } - instant_remote_process([ - "mkdir -p $dynamic_conf_path", - "echo '$base64' | base64 -d | tee $default_redirect_file > /dev/null", - ], $this); - if ($proxy_type === 'CADDY') { $this->reloadCaddy(); } @@ -612,6 +612,7 @@ $schema://$host { $memory = json_decode($memory, true); $parsedCollection = collect($memory)->map(function ($metric) { $usedPercent = $metric['usedPercent'] ?? 0.0; + return [(int) $metric['time'], (float) $usedPercent]; }); diff --git a/app/Models/Team.php b/app/Models/Team.php index e21aa3a25..29c469b3b 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -4,6 +4,7 @@ namespace App\Models; use App\Notifications\Channels\SendsDiscord; use App\Notifications\Channels\SendsEmail; +use App\Notifications\Channels\SendsSlack; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; 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; @@ -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) { $recipients = data_get($notification, 'emails', null); @@ -161,7 +167,7 @@ class Team extends Model implements SendsDiscord, SendsEmail return 9999999; } $team = Team::find(currentTeam()->id); - if (! $team) { + if (!$team) { return 0; } diff --git a/app/Notifications/Application/DeploymentFailed.php b/app/Notifications/Application/DeploymentFailed.php index ce1f99d77..cdcec3261 100644 --- a/app/Notifications/Application/DeploymentFailed.php +++ b/app/Notifications/Application/DeploymentFailed.php @@ -6,6 +6,7 @@ use App\Models\Application; use App\Models\ApplicationPreview; use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; +use App\Notifications\Dto\SlackMessage; use Illuminate\Notifications\Messages\MailMessage; class DeploymentFailed extends CustomEmailNotification @@ -39,7 +40,7 @@ class DeploymentFailed extends CustomEmailNotification if (str($this->fqdn)->explode(',')->count() > 1) { $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 @@ -53,10 +54,10 @@ class DeploymentFailed extends CustomEmailNotification $pull_request_id = data_get($this->preview, 'pull_request_id', 0); $fqdn = $this->fqdn; if ($pull_request_id === 0) { - $mail->subject('Coolify: Deployment failed of '.$this->application_name.'.'); + $mail->subject('Coolify: Deployment failed of ' . $this->application_name . '.'); } else { $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', [ 'name' => $this->application_name, @@ -73,7 +74,7 @@ class DeploymentFailed extends CustomEmailNotification if ($this->preview) { $message = new DiscordMessage( title: ':cross_mark: Deployment failed', - description: 'Pull request: '.$this->preview->pull_request_id, + description: 'Pull request: ' . $this->preview->pull_request_id, color: DiscordMessage::errorColor(), isCritical: true, ); @@ -82,13 +83,13 @@ class DeploymentFailed extends CustomEmailNotification $message->addField('Environment', $this->environment_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) { $message->addField('Domain', $this->fqdn, true); } } else { if ($this->fqdn) { - $description = '[Open application]('.$this->fqdn.')'; + $description = '[Open application](' . $this->fqdn . ')'; } else { $description = ''; } @@ -103,7 +104,7 @@ class DeploymentFailed extends CustomEmailNotification $message->addField('Environment', $this->environment_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; @@ -112,9 +113,9 @@ class DeploymentFailed extends CustomEmailNotification public function toTelegram(): array { 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 { - $message = 'Coolify: Deployment failed of '.$this->application_name.' ('.$this->fqdn.'): '; + $message = 'Coolify: Deployment failed of ' . $this->application_name . ' (' . $this->fqdn . '): '; } $buttons[] = [ 'text' => 'Deployment logs', @@ -128,4 +129,31 @@ class DeploymentFailed extends CustomEmailNotification ], ]; } + + 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() + ); + } } diff --git a/app/Notifications/Application/DeploymentSuccess.php b/app/Notifications/Application/DeploymentSuccess.php index 391601257..565bce0a5 100644 --- a/app/Notifications/Application/DeploymentSuccess.php +++ b/app/Notifications/Application/DeploymentSuccess.php @@ -7,6 +7,7 @@ use App\Models\ApplicationPreview; use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; use Illuminate\Notifications\Messages\MailMessage; +use App\Notifications\Dto\SlackMessage; class DeploymentSuccess extends CustomEmailNotification { @@ -39,7 +40,7 @@ class DeploymentSuccess extends CustomEmailNotification if (str($this->fqdn)->explode(',')->count() > 1) { $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 @@ -79,21 +80,21 @@ class DeploymentSuccess extends CustomEmailNotification if ($this->preview) { $message = new DiscordMessage( 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(), ); 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('Environment', $this->environment_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 { if ($this->fqdn) { - $description = '[Open application]('.$this->fqdn.')'; + $description = '[Open application](' . $this->fqdn . ')'; } else { $description = ''; } @@ -106,7 +107,7 @@ class DeploymentSuccess extends CustomEmailNotification $message->addField('Environment', $this->environment_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; @@ -115,7 +116,7 @@ class DeploymentSuccess extends CustomEmailNotification public function toTelegram(): array { 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) { $buttons[] = [ 'text' => 'Open Application', @@ -123,7 +124,7 @@ class DeploymentSuccess extends CustomEmailNotification ]; } } else { - $message = '✅ New version successfully deployed of '.$this->application_name.''; + $message = '✅ New version successfully deployed of ' . $this->application_name . ''; if ($this->fqdn) { $buttons[] = [ 'text' => 'Open Application', @@ -143,4 +144,32 @@ class DeploymentSuccess extends CustomEmailNotification ], ]; } + + + 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() + ); + } } diff --git a/app/Notifications/Application/StatusChanged.php b/app/Notifications/Application/StatusChanged.php index c757495cb..18c042ca6 100644 --- a/app/Notifications/Application/StatusChanged.php +++ b/app/Notifications/Application/StatusChanged.php @@ -6,6 +6,7 @@ use App\Models\Application; use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; use Illuminate\Notifications\Messages\MailMessage; +use App\Notifications\Dto\SlackMessage; class StatusChanged extends CustomEmailNotification { @@ -29,7 +30,7 @@ class StatusChanged extends CustomEmailNotification if (str($this->fqdn)->explode(',')->count() > 1) { $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 @@ -55,7 +56,7 @@ class StatusChanged extends CustomEmailNotification { return new DiscordMessage( title: ':cross_mark: Application stopped', - description: '[Open Application in Coolify]('.$this->resource_url.')', + description: '[Open Application in Coolify](' . $this->resource_url . ')', color: DiscordMessage::errorColor(), isCritical: true, ); @@ -63,7 +64,7 @@ class StatusChanged extends CustomEmailNotification public function toTelegram(): array { - $message = 'Coolify: '.$this->resource_name.' has been stopped.'; + $message = 'Coolify: ' . $this->resource_name . ' has been stopped.'; return [ 'message' => $message, @@ -75,4 +76,20 @@ class StatusChanged extends CustomEmailNotification ], ]; } + + 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() + ); + } } diff --git a/app/Notifications/Channels/SendsSlack.php b/app/Notifications/Channels/SendsSlack.php new file mode 100644 index 000000000..417d4adda --- /dev/null +++ b/app/Notifications/Channels/SendsSlack.php @@ -0,0 +1,8 @@ +toSlack(); + $webhookUrl = $notifiable->routeNotificationForSlack(); + if (! $webhookUrl) { + return; + } + SendMessageToSlackJob::dispatch($message, $webhookUrl); + } +} diff --git a/app/Notifications/Container/ContainerRestarted.php b/app/Notifications/Container/ContainerRestarted.php index eb709535f..e1cf836ff 100644 --- a/app/Notifications/Container/ContainerRestarted.php +++ b/app/Notifications/Container/ContainerRestarted.php @@ -6,6 +6,7 @@ use App\Models\Server; use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; use Illuminate\Notifications\Messages\MailMessage; +use App\Notifications\Dto\SlackMessage; class ContainerRestarted extends CustomEmailNotification { @@ -41,7 +42,7 @@ class ContainerRestarted extends CustomEmailNotification ); if ($this->url) { - $message->addField('Resource', '[Link]('.$this->url.')'); + $message->addField('Resource', '[Link](' . $this->url . ')'); } return $message; @@ -66,4 +67,20 @@ class ContainerRestarted extends CustomEmailNotification 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() + ); + } } diff --git a/app/Notifications/Container/ContainerStopped.php b/app/Notifications/Container/ContainerStopped.php index a73e984a0..76c2dae80 100644 --- a/app/Notifications/Container/ContainerStopped.php +++ b/app/Notifications/Container/ContainerStopped.php @@ -6,6 +6,7 @@ use App\Models\Server; use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; use Illuminate\Notifications\Messages\MailMessage; +use App\Notifications\Dto\SlackMessage; class ContainerStopped extends CustomEmailNotification { @@ -41,7 +42,7 @@ class ContainerStopped extends CustomEmailNotification ); if ($this->url) { - $message->addField('Resource', '[Link]('.$this->url.')'); + $message->addField('Resource', '[Link](' . $this->url . ')'); } return $message; @@ -66,4 +67,20 @@ class ContainerStopped extends CustomEmailNotification 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() + ); + } } diff --git a/app/Notifications/Database/BackupFailed.php b/app/Notifications/Database/BackupFailed.php index beeea0804..a1b5c42e5 100644 --- a/app/Notifications/Database/BackupFailed.php +++ b/app/Notifications/Database/BackupFailed.php @@ -6,6 +6,7 @@ use App\Models\ScheduledDatabaseBackup; use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; use Illuminate\Notifications\Messages\MailMessage; +use App\Notifications\Dto\SlackMessage; class BackupFailed extends CustomEmailNotification { @@ -62,4 +63,19 @@ class BackupFailed extends CustomEmailNotification '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() + ); + } } diff --git a/app/Notifications/Database/BackupSuccess.php b/app/Notifications/Database/BackupSuccess.php index d8bab069b..f2aa247a6 100644 --- a/app/Notifications/Database/BackupSuccess.php +++ b/app/Notifications/Database/BackupSuccess.php @@ -6,6 +6,7 @@ use App\Models\ScheduledDatabaseBackup; use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; use Illuminate\Notifications\Messages\MailMessage; +use App\Notifications\Dto\SlackMessage; class BackupSuccess extends CustomEmailNotification { @@ -60,4 +61,18 @@ class BackupSuccess extends CustomEmailNotification '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() + ); + } } diff --git a/app/Notifications/Dto/SlackMessage.php b/app/Notifications/Dto/SlackMessage.php new file mode 100644 index 000000000..86532c65b --- /dev/null +++ b/app/Notifications/Dto/SlackMessage.php @@ -0,0 +1,33 @@ + $this->message, ]; } + + public function toSlack(): SlackMessage + { + return new SlackMessage( + title: 'Coolify: General Notification', + description: $this->message, + color: SlackMessage::infoColor(), + ); + } } diff --git a/app/Notifications/ScheduledTask/TaskFailed.php b/app/Notifications/ScheduledTask/TaskFailed.php index 701f61277..53b7f46a3 100644 --- a/app/Notifications/ScheduledTask/TaskFailed.php +++ b/app/Notifications/ScheduledTask/TaskFailed.php @@ -6,6 +6,7 @@ use App\Models\ScheduledTask; use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; use Illuminate\Notifications\Messages\MailMessage; +use App\Notifications\Dto\SlackMessage; class TaskFailed extends CustomEmailNotification { @@ -48,7 +49,7 @@ class TaskFailed extends CustomEmailNotification ); if ($this->url) { - $message->addField('Scheduled task', '[Link]('.$this->url.')'); + $message->addField('Scheduled task', '[Link](' . $this->url . ')'); } return $message; @@ -68,4 +69,24 @@ class TaskFailed extends CustomEmailNotification '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() + ); + } } diff --git a/app/Notifications/Server/DockerCleanup.php b/app/Notifications/Server/DockerCleanup.php index 2d007a262..46b730c7b 100644 --- a/app/Notifications/Server/DockerCleanup.php +++ b/app/Notifications/Server/DockerCleanup.php @@ -7,6 +7,7 @@ use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\TelegramChannel; use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; +use App\Notifications\Dto\SlackMessage; class DockerCleanup extends CustomEmailNotification { @@ -21,7 +22,7 @@ class DockerCleanup extends CustomEmailNotification // $isEmailEnabled = isEmailEnabled($notifiable); $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isTelegramEnabled = data_get($notifiable, 'telegram_enabled'); - + $isSlackEnabled = data_get($notifiable, 'slack_enabled'); if ($isDiscordEnabled) { $channels[] = DiscordChannel::class; } @@ -31,6 +32,9 @@ class DockerCleanup extends CustomEmailNotification if ($isTelegramEnabled) { $channels[] = TelegramChannel::class; } + if ($isSlackEnabled) { + $channels[] = SlackChannel::class; + } return $channels; } @@ -62,4 +66,13 @@ class DockerCleanup extends CustomEmailNotification '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() + ); + } } diff --git a/app/Notifications/Server/ForceDisabled.php b/app/Notifications/Server/ForceDisabled.php index eabf8b334..1939170f6 100644 --- a/app/Notifications/Server/ForceDisabled.php +++ b/app/Notifications/Server/ForceDisabled.php @@ -6,6 +6,8 @@ use App\Models\Server; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\TelegramChannel; +use App\Notifications\Channels\SlackChannel; +use App\Notifications\Dto\SlackMessage; use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; use Illuminate\Notifications\Messages\MailMessage; @@ -23,7 +25,7 @@ class ForceDisabled extends CustomEmailNotification $isEmailEnabled = isEmailEnabled($notifiable); $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isTelegramEnabled = data_get($notifiable, 'telegram_enabled'); - + $isSlackEnabled = data_get($notifiable, 'slack_enabled'); if ($isDiscordEnabled) { $channels[] = DiscordChannel::class; } @@ -33,6 +35,9 @@ class ForceDisabled extends CustomEmailNotification if ($isTelegramEnabled) { $channels[] = TelegramChannel::class; } + if ($isSlackEnabled) { + $channels[] = SlackChannel::class; + } return $channels; } @@ -67,4 +72,19 @@ class ForceDisabled extends CustomEmailNotification '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() + ); + } } diff --git a/app/Notifications/Server/ForceEnabled.php b/app/Notifications/Server/ForceEnabled.php index 0c21ed6b8..4b6181e19 100644 --- a/app/Notifications/Server/ForceEnabled.php +++ b/app/Notifications/Server/ForceEnabled.php @@ -6,6 +6,8 @@ use App\Models\Server; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\TelegramChannel; +use App\Notifications\Channels\SlackChannel; +use App\Notifications\Dto\SlackMessage; use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; use Illuminate\Notifications\Messages\MailMessage; @@ -23,7 +25,7 @@ class ForceEnabled extends CustomEmailNotification $isEmailEnabled = isEmailEnabled($notifiable); $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isTelegramEnabled = data_get($notifiable, 'telegram_enabled'); - + $isSlackEnabled = data_get($notifiable, 'slack_enabled'); if ($isDiscordEnabled) { $channels[] = DiscordChannel::class; } @@ -33,6 +35,9 @@ class ForceEnabled extends CustomEmailNotification if ($isTelegramEnabled) { $channels[] = TelegramChannel::class; } + if ($isSlackEnabled) { + $channels[] = SlackChannel::class; + } return $channels; } @@ -63,4 +68,15 @@ class ForceEnabled extends CustomEmailNotification '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() + ); + } + } diff --git a/app/Notifications/Server/HighDiskUsage.php b/app/Notifications/Server/HighDiskUsage.php index 7cec2e892..40c0e2cb6 100644 --- a/app/Notifications/Server/HighDiskUsage.php +++ b/app/Notifications/Server/HighDiskUsage.php @@ -5,6 +5,7 @@ namespace App\Notifications\Server; use App\Models\Server; use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; +use App\Notifications\Dto\SlackMessage; use Illuminate\Notifications\Messages\MailMessage; class HighDiskUsage extends CustomEmailNotification @@ -44,7 +45,7 @@ class HighDiskUsage extends CustomEmailNotification $message->addField('Disk usage', "{$this->disk_usage}%", true); $message->addField('Threshold', "{$this->server_disk_usage_notification_threshold}%", true); $message->addField('What to do?', '[Link](https://coolify.io/docs/knowledge-base/server/automated-cleanup)', true); - $message->addField('Change Settings', '[Threshold]('.base_url().'/server/'.$this->server->uuid.'#advanced) | [Notification]('.base_url().'/notifications/discord)'); + $message->addField('Change Settings', '[Threshold](' . base_url() . '/server/' . $this->server->uuid . '#advanced) | [Notification](' . base_url() . '/notifications/discord)'); return $message; } @@ -55,4 +56,22 @@ class HighDiskUsage extends CustomEmailNotification '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() + ); + } } diff --git a/app/Notifications/Server/Reachable.php b/app/Notifications/Server/Reachable.php index 44189c3b5..a0dfd1cc5 100644 --- a/app/Notifications/Server/Reachable.php +++ b/app/Notifications/Server/Reachable.php @@ -6,6 +6,8 @@ use App\Models\Server; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\TelegramChannel; +use App\Notifications\Channels\SlackChannel; +use App\Notifications\Dto\SlackMessage; use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; use Illuminate\Notifications\Messages\MailMessage; @@ -18,7 +20,7 @@ class Reachable extends CustomEmailNotification { $this->onQueue('high'); $this->isRateLimited = isEmailRateLimited( - limiterKey: 'server-reachable:'.$this->server->id, + limiterKey: 'server-reachable:' . $this->server->id, ); } @@ -32,7 +34,7 @@ class Reachable extends CustomEmailNotification $isEmailEnabled = isEmailEnabled($notifiable); $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isTelegramEnabled = data_get($notifiable, 'telegram_enabled'); - + $isSlackEnabled = data_get($notifiable, 'slack_enabled'); if ($isDiscordEnabled) { $channels[] = DiscordChannel::class; } @@ -42,6 +44,9 @@ class Reachable extends CustomEmailNotification if ($isTelegramEnabled) { $channels[] = TelegramChannel::class; } + if ($isSlackEnabled) { + $channels[] = SlackChannel::class; + } return $channels; } @@ -72,4 +77,16 @@ class Reachable extends CustomEmailNotification '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() + ); + } } diff --git a/app/Notifications/Server/Unreachable.php b/app/Notifications/Server/Unreachable.php index 6fb792bdc..0bd44ef3d 100644 --- a/app/Notifications/Server/Unreachable.php +++ b/app/Notifications/Server/Unreachable.php @@ -6,6 +6,8 @@ use App\Models\Server; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\TelegramChannel; +use App\Notifications\Channels\SlackChannel; +use App\Notifications\Dto\SlackMessage; use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; use Illuminate\Notifications\Messages\MailMessage; @@ -18,7 +20,7 @@ class Unreachable extends CustomEmailNotification { $this->onQueue('high'); $this->isRateLimited = isEmailRateLimited( - limiterKey: 'server-unreachable:'.$this->server->id, + limiterKey: 'server-unreachable:' . $this->server->id, ); } @@ -32,6 +34,7 @@ class Unreachable extends CustomEmailNotification $isEmailEnabled = isEmailEnabled($notifiable); $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isTelegramEnabled = data_get($notifiable, 'telegram_enabled'); + $isSlackEnabled = data_get($notifiable, 'slack_enabled'); if ($isDiscordEnabled) { $channels[] = DiscordChannel::class; @@ -42,6 +45,9 @@ class Unreachable extends CustomEmailNotification if ($isTelegramEnabled) { $channels[] = TelegramChannel::class; } + if ($isSlackEnabled) { + $channels[] = SlackChannel::class; + } return $channels; } @@ -76,4 +82,18 @@ class Unreachable extends CustomEmailNotification '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() + ); + } } diff --git a/app/Notifications/Test.php b/app/Notifications/Test.php index 64f9bb0a5..03f6c3296 100644 --- a/app/Notifications/Test.php +++ b/app/Notifications/Test.php @@ -3,6 +3,7 @@ namespace App\Notifications; use App\Notifications\Dto\DiscordMessage; +use App\Notifications\Dto\SlackMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -67,4 +68,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.' + ); + } } diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php index a8ef0fe5a..463e89b6f 100644 --- a/bootstrap/helpers/proxy.php +++ b/bootstrap/helpers/proxy.php @@ -173,13 +173,12 @@ function generate_default_proxy_configuration(Server $server) ], 'volumes' => [ '/var/run/docker.sock:/var/run/docker.sock:ro', - "{$proxy_path}:/traefik", + ], 'command' => [ '--ping=true', '--ping.entrypoint=http', '--api.dashboard=true', - '--api.insecure=false', '--entrypoints.http.address=:80', '--entrypoints.https.address=:443', '--entrypoints.http.http.encodequerysemicolons=true', @@ -187,21 +186,26 @@ function generate_default_proxy_configuration(Server $server) '--entrypoints.https.http.encodequerysemicolons=true', '--entryPoints.https.http2.maxConcurrentStreams=50', '--entrypoints.https.http3', - '--providers.docker.exposedbydefault=false', '--providers.file.directory=/traefik/dynamic/', + '--providers.docker.exposedbydefault=false', '--providers.file.watch=true', '--certificatesresolvers.letsencrypt.acme.httpchallenge=true', - '--certificatesresolvers.letsencrypt.acme.storage=/traefik/acme.json', '--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=http', + '--certificatesresolvers.letsencrypt.acme.storage=/traefik/acme.json', ], 'labels' => $labels, ], ], ]; if (isDev()) { - // $config['services']['traefik']['command'][] = "--log.level=debug"; + $config['services']['traefik']['command'][] = '--api.insecure=true'; + $config['services']['traefik']['command'][] = '--log.level=debug'; $config['services']['traefik']['command'][] = '--accesslog.filepath=/traefik/access.log'; $config['services']['traefik']['command'][] = '--accesslog.bufferingsize=100'; + $config['services']['traefik']['volumes'][] = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/:/traefik'; + } else { + $config['services']['traefik']['command'][] = '--api.insecure=false'; + $config['services']['traefik']['volumes'][] = "{$proxy_path}:/traefik"; } if ($server->isSwarm()) { data_forget($config, 'services.traefik.container_name'); diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index a3ef93dfc..40c5acb21 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -27,6 +27,7 @@ use App\Models\Team; use App\Models\User; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; +use App\Notifications\Channels\SlackChannel; use App\Notifications\Channels\TelegramChannel; use App\Notifications\Internal\GeneralNotification; use Carbon\CarbonImmutable; @@ -67,27 +68,27 @@ function base_configuration_dir(): string } function application_configuration_dir(): string { - return base_configuration_dir().'/applications'; + return base_configuration_dir() . '/applications'; } function service_configuration_dir(): string { - return base_configuration_dir().'/services'; + return base_configuration_dir() . '/services'; } function database_configuration_dir(): string { - return base_configuration_dir().'/databases'; + return base_configuration_dir() . '/databases'; } function database_proxy_dir($uuid): string { - return base_configuration_dir()."/databases/$uuid/proxy"; + return base_configuration_dir() . "/databases/$uuid/proxy"; } function backup_dir(): string { - return base_configuration_dir().'/backups'; + return base_configuration_dir() . '/backups'; } function metrics_dir(): string { - return base_configuration_dir().'/metrics'; + return base_configuration_dir() . '/metrics'; } function sanitize_string(?string $input = null): ?string @@ -138,15 +139,15 @@ function showBoarding(): bool } function refreshSession(?Team $team = null): void { - if (! $team) { + if (!$team) { if (Auth::user()->currentTeam()) { $team = Team::find(Auth::user()->currentTeam()->id); } else { $team = User::find(Auth::id())->teams->first(); } } - Cache::forget('team:'.Auth::id()); - Cache::remember('team:'.Auth::id(), 3600, function () use ($team) { + Cache::forget('team:' . Auth::id()); + Cache::remember('team:' . Auth::id(), 3600, function () use ($team) { return $team; }); session(['currentTeam' => $team]); @@ -178,7 +179,7 @@ function handleError(?Throwable $error = null, ?Livewire\Component $livewire = n $message = null; } if ($customErrorMessage) { - $message = $customErrorMessage.' '.$message; + $message = $customErrorMessage . ' ' . $message; } if (isset($livewire)) { @@ -251,7 +252,7 @@ function generateSSHKey(string $type = 'rsa') function formatPrivateKey(string $privateKey) { $privateKey = trim($privateKey); - if (! str_ends_with($privateKey, "\n")) { + if (!str_ends_with($privateKey, "\n")) { $privateKey .= "\n"; } @@ -273,7 +274,7 @@ function is_transactional_emails_active(): bool function set_transanctional_email_settings(?InstanceSettings $settings = null): ?string { - if (! $settings) { + if (!$settings) { $settings = instanceSettings(); } config()->set('mail.from.address', data_get($settings, 'smtp_from_address')); @@ -373,7 +374,7 @@ function isSubscribed() function isProduction(): bool { - return ! isDev(); + return !isDev(); } function isDev(): bool { @@ -426,14 +427,14 @@ function send_user_an_email(MailMessage $mail, string $email, ?string $cc = null { $settings = instanceSettings(); $type = set_transanctional_email_settings($settings); - if (! $type) { + if (!$type) { throw new Exception('No email settings found.'); } if ($cc) { Mail::send( [], [], - fn (Message $message) => $message + fn(Message $message) => $message ->to($email) ->replyTo($email) ->cc($cc) @@ -444,7 +445,7 @@ function send_user_an_email(MailMessage $mail, string $email, ?string $cc = null Mail::send( [], [], - fn (Message $message) => $message + fn(Message $message) => $message ->to($email) ->subject($mail->subject) ->html((string) $mail->render()) @@ -469,11 +470,13 @@ function setNotificationChannels($notifiable, $event) { $channels = []; $isEmailEnabled = isEmailEnabled($notifiable); + $isSlackEnabled = data_get($notifiable, 'slack_enabled'); $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isTelegramEnabled = data_get($notifiable, 'telegram_enabled'); $isSubscribedToEmailEvent = data_get($notifiable, "smtp_notifications_$event"); $isSubscribedToDiscordEvent = data_get($notifiable, "discord_notifications_$event"); $isSubscribedToTelegramEvent = data_get($notifiable, "telegram_notifications_$event"); + $isSubscribedToSlackEvent = data_get($notifiable, "slack_notifications_$event"); if ($isDiscordEnabled && $isSubscribedToDiscordEvent) { $channels[] = DiscordChannel::class; @@ -484,6 +487,9 @@ function setNotificationChannels($notifiable, $event) if ($isTelegramEnabled && $isSubscribedToTelegramEvent) { $channels[] = TelegramChannel::class; } + if ($isSlackEnabled && $isSubscribedToSlackEvent) { + $channels[] = SlackChannel::class; + } return $channels; } @@ -588,7 +594,7 @@ function getResourceByUuid(string $uuid, ?int $teamId = null) return null; } $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; } @@ -680,29 +686,29 @@ function queryResourcesByUuid(string $uuid) function generateTagDeployWebhook($tag_name) { $baseUrl = base_url(); - $api = Url::fromString($baseUrl).'/api/v1'; + $api = Url::fromString($baseUrl) . '/api/v1'; $endpoint = "/deploy?tag=$tag_name"; - return $api.$endpoint; + return $api . $endpoint; } function generateDeployWebhook($resource) { $baseUrl = base_url(); - $api = Url::fromString($baseUrl).'/api/v1'; + $api = Url::fromString($baseUrl) . '/api/v1'; $endpoint = '/deploy'; $uuid = data_get($resource, 'uuid'); - return $api.$endpoint."?uuid=$uuid&force=false"; + return $api . $endpoint . "?uuid=$uuid&force=false"; } 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; } if ($resource->getMorphClass() === \App\Models\Application::class) { $baseUrl = base_url(); - return Url::fromString($baseUrl)."/webhooks/source/$type/events/manual"; + return Url::fromString($baseUrl) . "/webhooks/source/$type/events/manual"; } return null; @@ -729,7 +735,7 @@ function getTopLevelNetworks(Service|Application $resource) $hasHostNetworkMode = data_get($service, 'network_mode') === 'host' ? true : false; // Only add 'networks' key if 'network_mode' is not 'host' - if (! $hasHostNetworkMode) { + if (!$hasHostNetworkMode) { // Collect/create/update networks if ($serviceNetworks->count() > 0) { foreach ($serviceNetworks as $networkName => $networkDetails) { @@ -743,7 +749,7 @@ function getTopLevelNetworks(Service|Application $resource) $networkExists = $topLevelNetworks->contains(function ($value, $key) use ($networkName) { return $value == $networkName || $key == $networkName; }); - if (! $networkExists) { + if (!$networkExists) { if (is_string($networkDetails) || is_int($networkDetails)) { $topLevelNetworks->put($networkDetails, null); } @@ -754,7 +760,7 @@ function getTopLevelNetworks(Service|Application $resource) $definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) { return $value == $definedNetwork; }); - if (! $definedNetworkExists) { + if (!$definedNetworkExists) { foreach ($definedNetwork as $network) { $topLevelNetworks->put($network, [ 'name' => $network, @@ -795,7 +801,7 @@ function getTopLevelNetworks(Service|Application $resource) $networkExists = $topLevelNetworks->contains(function ($value, $key) use ($networkName) { return $value == $networkName || $key == $networkName; }); - if (! $networkExists) { + if (!$networkExists) { if (is_string($networkDetails) || is_int($networkDetails)) { $topLevelNetworks->put($networkDetails, null); } @@ -805,7 +811,7 @@ function getTopLevelNetworks(Service|Application $resource) $definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) { return $value == $definedNetwork; }); - if (! $definedNetworkExists) { + if (!$definedNetworkExists) { foreach ($definedNetwork as $network) { $topLevelNetworks->put($network, [ 'name' => $network, @@ -941,7 +947,7 @@ function generateEnvValue(string $command, Service|Application|null $service = n case 'PASSWORD_64': $generatedValue = Str::password(length: 64, symbols: false); break; - // This is not base64, it's just a random string + // This is not base64, it's just a random string case 'BASE64_64': $generatedValue = Str::random(64); break; @@ -952,7 +958,7 @@ function generateEnvValue(string $command, Service|Application|null $service = n case 'BASE64_32': $generatedValue = Str::random(32); break; - // This is base64, + // This is base64, case 'REALBASE64_64': $generatedValue = base64_encode(Str::random(64)); break; @@ -1052,7 +1058,7 @@ function validate_dns_entry(string $fqdn, Server $server) } $settings = instanceSettings(); $is_dns_validation_enabled = data_get($settings, 'is_dns_validation_enabled'); - if (! $is_dns_validation_enabled) { + if (!$is_dns_validation_enabled) { return true; } $dns_servers = data_get($settings, 'custom_dns_servers'); @@ -1070,7 +1076,7 @@ function validate_dns_entry(string $fqdn, Server $server) $query = new DNSQuery($dns_server); $results = $query->query($host, $type); if ($results === false || $query->hasError()) { - ray('Error: '.$query->getLasterror()); + ray('Error: ' . $query->getLasterror()); } else { foreach ($results as $result) { if ($result->getType() == $type) { @@ -1080,7 +1086,7 @@ function validate_dns_entry(string $fqdn, Server $server) break; } if ($result->getData() === $ip) { - ray($host.' has IP address '.$result->getData()); + ray($host . ' has IP address ' . $result->getData()); ray($result->getString()); $found_matching_ip = true; break; @@ -1128,15 +1134,15 @@ function checkIfDomainIsAlreadyUsed(Collection|array $domains, ?string $teamId = $applications = Application::ownedByCurrentTeamAPI($teamId)->get(['fqdn', 'uuid']); $serviceApplications = ServiceApplication::ownedByCurrentTeamAPI($teamId)->get(['fqdn', 'uuid']); if ($uuid) { - $applications = $applications->filter(fn ($app) => $app->uuid !== $uuid); - $serviceApplications = $serviceApplications->filter(fn ($app) => $app->uuid !== $uuid); + $applications = $applications->filter(fn($app) => $app->uuid !== $uuid); + $serviceApplications = $serviceApplications->filter(fn($app) => $app->uuid !== $uuid); } $domainFound = false; foreach ($applications as $app) { if (is_null($app->fqdn)) { 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) { if (str($domain)->endsWith('/')) { $domain = str($domain)->beforeLast('/'); @@ -1155,7 +1161,7 @@ function checkIfDomainIsAlreadyUsed(Collection|array $domains, ?string $teamId = if (str($app->fqdn)->isEmpty()) { 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) { if (str($domain)->endsWith('/')) { $domain = str($domain)->beforeLast('/'); @@ -1205,7 +1211,7 @@ function check_domain_usage(ServiceApplication|Application|null $resource = null }); $apps = Application::all(); 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) { if (str($domain)->endsWith('/')) { $domain = str($domain)->beforeLast('/'); @@ -1224,7 +1230,7 @@ function check_domain_usage(ServiceApplication|Application|null $resource = null } $apps = ServiceApplication::all(); 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) { if (str($domain)->endsWith('/')) { $domain = str($domain)->beforeLast('/'); @@ -1260,7 +1266,7 @@ function parseCommandsByLineForSudo(Collection $commands, Server $server): array { $commands = $commands->map(function ($line) { if ( - ! str(trim($line))->startsWith([ + !str(trim($line))->startsWith([ 'cd', 'command', 'echo', @@ -1281,7 +1287,7 @@ function parseCommandsByLineForSudo(Collection $commands, Server $server): array $commands = $commands->map(function ($line) use ($server) { 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; @@ -1309,11 +1315,11 @@ function parseCommandsByLineForSudo(Collection $commands, Server $server): array } 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"; } 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('`')) { $command = str($command)->replace('$(', '$(sudo ')->replace('`', '`sudo ')->value(); @@ -1435,7 +1441,7 @@ function parseServiceVolumes($serviceVolumes, $resource, $topLevelVolumes, $pull $isDirectory = data_get($foundConfig, 'is_directory'); } else { $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 ray('setting isDirectory to true'); $isDirectory = true; @@ -1450,9 +1456,9 @@ function parseServiceVolumes($serviceVolumes, $resource, $topLevelVolumes, $pull return $volume; } if (get_class($resource) === \App\Models\Application::class) { - $dir = base_configuration_dir().'/applications/'.$resource->uuid; + $dir = base_configuration_dir() . '/applications/' . $resource->uuid; } else { - $dir = base_configuration_dir().'/services/'.$resource->service->uuid; + $dir = base_configuration_dir() . '/services/' . $resource->service->uuid; } if ($source->startsWith('.')) { @@ -1462,9 +1468,9 @@ function parseServiceVolumes($serviceVolumes, $resource, $topLevelVolumes, $pull $source = $source->replaceFirst('~', $dir); } 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( [ 'mount_path' => $target, @@ -1593,7 +1599,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal if ($serviceLabels->count() > 0) { $removedLabels = collect([]); $serviceLabels = $serviceLabels->filter(function ($serviceLabel, $serviceLabelName) use ($removedLabels) { - if (! str($serviceLabel)->contains('=')) { + if (!str($serviceLabel)->contains('=')) { $removedLabels->put($serviceLabelName, $serviceLabel); return false; @@ -1675,7 +1681,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $networkExists = $topLevelNetworks->contains(function ($value, $key) use ($networkName) { return $value == $networkName || $key == $networkName; }); - if (! $networkExists) { + if (!$networkExists) { if (is_string($networkDetails) || is_int($networkDetails)) { $topLevelNetworks->put($networkDetails, null); } @@ -1701,12 +1707,12 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $savedService->ports = $collectedPorts->implode(','); $savedService->save(); - if (! $hasHostNetworkMode) { + if (!$hasHostNetworkMode) { // Add Coolify specific networks $definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) { return $value == $definedNetwork; }); - if (! $definedNetworkExists) { + if (!$definedNetworkExists) { foreach ($definedNetwork as $network) { $topLevelNetworks->put($network, [ 'name' => $network, @@ -1918,9 +1924,9 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $fqdn = "$fqdn$path"; } - if (! $isDatabase) { + if (!$isDatabase) { if ($savedService->fqdn) { - data_set($savedService, 'fqdn', $savedService->fqdn.','.$fqdn); + data_set($savedService, 'fqdn', $savedService->fqdn . ',' . $fqdn); } else { data_set($savedService, 'fqdn', $fqdn); } @@ -1935,7 +1941,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal ]); } // Caddy needs exact port in some cases. - if ($predefinedPort && ! $key->endsWith("_{$predefinedPort}")) { + if ($predefinedPort && !$key->endsWith("_{$predefinedPort}")) { $fqdns_exploded = str($savedService->fqdn)->explode(','); if ($fqdns_exploded->count() > 1) { continue; @@ -1975,12 +1981,12 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal 'service_id' => $resource->id, ])->first(); ['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 (Str::lower($forService) === $serviceName) { $fqdn = generateFqdn($resource->server, $containerName); } else { - $fqdn = generateFqdn($resource->server, Str::lower($forService).'-'.$resource->uuid); + $fqdn = generateFqdn($resource->server, Str::lower($forService) . '-' . $resource->uuid); } if ($port) { $fqdn = "$fqdn:$port"; @@ -2010,13 +2016,13 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal 'is_preview' => false, ]); } - if (! $isDatabase) { - if ($command->value() === 'FQDN' && is_null($savedService->fqdn) && ! $foundEnv) { + if (!$isDatabase) { + if ($command->value() === 'FQDN' && is_null($savedService->fqdn) && !$foundEnv) { $savedService->fqdn = $fqdn; $savedService->save(); } // 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(','); if ($fqdns_exploded->count() > 1) { continue; @@ -2038,7 +2044,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } } else { $generatedValue = generateEnvValue($command, $resource); - if (! $foundEnv) { + if (!$foundEnv) { EnvironmentVariable::create([ 'key' => $key, 'value' => $generatedValue, @@ -2093,7 +2099,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } $defaultLabels = defaultLabels($resource->id, $containerName, type: 'service', subType: $isDatabase ? 'database' : 'application', subId: $savedService->id); $serviceLabels = $serviceLabels->merge($defaultLabels); - if (! $isDatabase && $fqdns->count() > 0) { + if (!$isDatabase && $fqdns->count() > 0) { if ($fqdns) { $shouldGenerateLabelsExactly = $resource->server->settings->generate_exact_labels; if ($shouldGenerateLabelsExactly) { @@ -2161,7 +2167,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } data_set($service, 'labels', $serviceLabels->toArray()); data_forget($service, 'is_database'); - if (! data_get($service, 'restart')) { + if (!data_get($service, 'restart')) { data_set($service, 'restart', RESTART_MODE); } if (data_get($service, 'restart') === 'no' || data_get($service, 'exclude_from_hc')) { @@ -2200,21 +2206,21 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $parsedServiceVariables->put('COOLIFY_CONTAINER_NAME', "$serviceName-{$resource->uuid}"); // 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}\""); } - if (! $parsedServiceVariables->has('COOLIFY_SERVER_IP')) { + if (!$parsedServiceVariables->has('COOLIFY_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}\""); } - if (! $parsedServiceVariables->has('COOLIFY_PROJECT_NAME')) { + if (!$parsedServiceVariables->has('COOLIFY_PROJECT_NAME')) { $parsedServiceVariables->put('COOLIFY_PROJECT_NAME', "\"{$resource->project()->name}\""); } $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(); if ($found_env) { return $found_env->value; @@ -2298,7 +2304,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal if ($serviceLabels->count() > 0) { $removedLabels = collect([]); $serviceLabels = $serviceLabels->filter(function ($serviceLabel, $serviceLabelName) use ($removedLabels) { - if (! str($serviceLabel)->contains('=')) { + if (!str($serviceLabel)->contains('=')) { $removedLabels->put($serviceLabelName, $serviceLabel); return false; @@ -2318,11 +2324,11 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $serviceVolumes = $serviceVolumes->map(function ($volume) use ($resource, $topLevelVolumes, $pull_request_id) { if (is_string($volume)) { $volume = str($volume); - if ($volume->contains(':') && ! $volume->startsWith('/')) { + if ($volume->contains(':') && !$volume->startsWith('/')) { $name = $volume->before(':'); $mount = $volume->after(':'); if ($name->startsWith('.') || $name->startsWith('~')) { - $dir = base_configuration_dir().'/applications/'.$resource->uuid; + $dir = base_configuration_dir() . '/applications/' . $resource->uuid; if ($name->startsWith('.')) { $name = $name->replaceFirst('.', $dir); } @@ -2330,12 +2336,12 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $name = $name->replaceFirst('~', $dir); } if ($pull_request_id !== 0) { - $name = $name."-pr-$pull_request_id"; + $name = $name . "-pr-$pull_request_id"; } $volume = str("$name:$mount"); } else { if ($pull_request_id !== 0) { - $name = $name."-pr-$pull_request_id"; + $name = $name . "-pr-$pull_request_id"; $volume = str("$name:$mount"); if ($topLevelVolumes->has($name)) { $v = $topLevelVolumes->get($name); @@ -2374,7 +2380,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $name = $volume->before(':'); $mount = $volume->after(':'); if ($pull_request_id !== 0) { - $name = $name."-pr-$pull_request_id"; + $name = $name . "-pr-$pull_request_id"; } $volume = str("$name:$mount"); } @@ -2385,7 +2391,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $read_only = data_get($volume, 'read_only'); if ($source && $target) { if ((str($source)->startsWith('.') || str($source)->startsWith('~'))) { - $dir = base_configuration_dir().'/applications/'.$resource->uuid; + $dir = base_configuration_dir() . '/applications/' . $resource->uuid; if (str($source, '.')) { $source = str($source)->replaceFirst('.', $dir); } @@ -2393,23 +2399,23 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $source = str($source)->replaceFirst('~', $dir); } if ($pull_request_id !== 0) { - $source = $source."-pr-$pull_request_id"; + $source = $source . "-pr-$pull_request_id"; } if ($read_only) { - data_set($volume, 'source', $source.':'.$target.':ro'); + data_set($volume, 'source', $source . ':' . $target . ':ro'); } else { - data_set($volume, 'source', $source.':'.$target); + data_set($volume, 'source', $source . ':' . $target); } } else { if ($pull_request_id !== 0) { - $source = $source."-pr-$pull_request_id"; + $source = $source . "-pr-$pull_request_id"; } if ($read_only) { - data_set($volume, 'source', $source.':'.$target.':ro'); + data_set($volume, 'source', $source . ':' . $target . ':ro'); } 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)) { $v = $topLevelVolumes->get($source); if (data_get($v, 'driver_opts.type') === 'cifs') { @@ -2442,11 +2448,11 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $serviceVolumes = $serviceVolumes->map(function ($volume) use ($resource, $topLevelVolumes, $pull_request_id) { if (is_string($volume)) { $volume = str($volume); - if ($volume->contains(':') && ! $volume->startsWith('/')) { + if ($volume->contains(':') && !$volume->startsWith('/')) { $name = $volume->before(':'); $mount = $volume->after(':'); if ($name->startsWith('.') || $name->startsWith('~')) { - $dir = base_configuration_dir().'/applications/'.$resource->uuid; + $dir = base_configuration_dir() . '/applications/' . $resource->uuid; if ($name->startsWith('.')) { $name = $name->replaceFirst('.', $dir); } @@ -2454,13 +2460,13 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $name = $name->replaceFirst('~', $dir); } if ($pull_request_id !== 0) { - $name = $name."-pr-$pull_request_id"; + $name = $name . "-pr-$pull_request_id"; } $volume = str("$name:$mount"); } else { if ($pull_request_id !== 0) { $uuid = $resource->uuid; - $name = $uuid."-$name-pr-$pull_request_id"; + $name = $uuid . "-$name-pr-$pull_request_id"; $volume = str("$name:$mount"); if ($topLevelVolumes->has($name)) { $v = $topLevelVolumes->get($name); @@ -2479,7 +2485,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } } else { $uuid = $resource->uuid; - $name = str($uuid."-$name"); + $name = str($uuid . "-$name"); $volume = str("$name:$mount"); if ($topLevelVolumes->has($name->value())) { $v = $topLevelVolumes->get($name->value()); @@ -2502,7 +2508,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $name = $volume->before(':'); $mount = $volume->after(':'); if ($pull_request_id !== 0) { - $name = $name."-pr-$pull_request_id"; + $name = $name . "-pr-$pull_request_id"; } $volume = str("$name:$mount"); } @@ -2514,7 +2520,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal if ($source && $target) { $uuid = $resource->uuid; 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, '.')) { $source = str($source)->replaceFirst('.', $dir); } @@ -2522,22 +2528,22 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $source = str($source)->replaceFirst('~', $dir); } if ($read_only) { - data_set($volume, 'source', $source.':'.$target.':ro'); + data_set($volume, 'source', $source . ':' . $target . ':ro'); } else { - data_set($volume, 'source', $source.':'.$target); + data_set($volume, 'source', $source . ':' . $target); } } else { if ($pull_request_id === 0) { - $source = $uuid."-$source"; + $source = $uuid . "-$source"; } else { - $source = $uuid."-$source-pr-$pull_request_id"; + $source = $uuid . "-$source-pr-$pull_request_id"; } if ($read_only) { - data_set($volume, 'source', $source.':'.$target.':ro'); + data_set($volume, 'source', $source . ':' . $target . ':ro'); } 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)) { $v = $topLevelVolumes->get($source); if (data_get($v, 'driver_opts.type') === 'cifs') { @@ -2570,7 +2576,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal if ($pull_request_id !== 0 && count($serviceDependencies) > 0) { $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()); } @@ -2592,7 +2598,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $networkExists = $topLevelNetworks->contains(function ($value, $key) use ($networkName) { return $value == $networkName || $key == $networkName; }); - if (! $networkExists) { + if (!$networkExists) { if (is_string($networkDetails) || is_int($networkDetails)) { $topLevelNetworks->put($networkDetails, null); } @@ -2620,7 +2626,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) { return $value == $definedNetwork; }); - if (! $definedNetworkExists) { + if (!$definedNetworkExists) { foreach ($definedNetwork as $network) { if ($pull_request_id !== 0) { $topLevelNetworks->put($network, [ @@ -2738,12 +2744,12 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal 'application_id' => $resource->id, ])->first(); ['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 (Str::lower($forService) === $serviceName) { $fqdn = generateFqdn($server, $containerName); } else { - $fqdn = generateFqdn($server, Str::lower($forService).'-'.$resource->uuid); + $fqdn = generateFqdn($server, Str::lower($forService) . '-' . $resource->uuid); } if ($port) { $fqdn = "$fqdn:$port"; @@ -2764,7 +2770,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } } else { $generatedValue = generateEnvValue($command); - if (! $foundEnv) { + if (!$foundEnv) { EnvironmentVariable::create([ 'key' => $key, 'value' => $generatedValue, @@ -2936,7 +2942,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } data_set($service, 'labels', $serviceLabels->toArray()); data_forget($service, 'is_database'); - if (! data_get($service, 'restart')) { + if (!data_get($service, 'restart')) { data_set($service, 'restart', RESTART_MODE); } data_set($service, 'container_name', $containerName); @@ -2947,7 +2953,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal }); if ($pull_request_id !== 0) { $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); }); } @@ -2975,7 +2981,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $uuid = data_get($resource, 'uuid'); $compose = data_get($resource, 'docker_compose_raw'); - if (! $compose) { + if (!$compose) { return collect([]); } @@ -3368,29 +3374,29 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $isDirectory = data_get($foundConfig, 'is_directory'); } else { // 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; } } } if ($type->value() === 'bind') { if ($source->value() === '/var/run/docker.sock') { - $volume = $source->value().':'.$target->value(); + $volume = $source->value() . ':' . $target->value(); } elseif ($source->value() === '/tmp' || $source->value() === '/tmp/') { - $volume = $source->value().':'.$target->value(); + $volume = $source->value() . ':' . $target->value(); } else { if ((int) $resource->compose_parsing_version >= 4) { if ($isApplication) { - $mainDirectory = str(base_configuration_dir().'/applications/'.$uuid); + $mainDirectory = str(base_configuration_dir() . '/applications/' . $uuid); } elseif ($isService) { - $mainDirectory = str(base_configuration_dir().'/services/'.$uuid); + $mainDirectory = str(base_configuration_dir() . '/services/' . $uuid); } } else { - $mainDirectory = str(base_configuration_dir().'/applications/'.$uuid); + $mainDirectory = str(base_configuration_dir() . '/applications/' . $uuid); } $source = replaceLocalSource($source, $mainDirectory); if ($isApplication && $isPullRequest) { - $source = $source."-pr-$pullRequestId"; + $source = $source . "-pr-$pullRequestId"; } LocalFileVolume::updateOrCreate( [ @@ -3410,12 +3416,12 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int if (isDev()) { if ((int) $resource->compose_parsing_version >= 4) { 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) { - $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 { - $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"; @@ -3482,7 +3488,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $depends_on = $newDependsOn; } } - if (! $use_network_mode) { + if (!$use_network_mode) { if ($topLevel->get('networks')?->count() > 0) { foreach ($topLevel->get('networks') as $networkName => $network) { if ($networkName === 'default') { @@ -3495,7 +3501,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $networkExists = $networks->contains(function ($value, $key) use ($networkName) { return $value == $networkName || $key == $networkName; }); - if (! $networkExists) { + if (!$networkExists) { $networks->put($networkName, null); } } @@ -3503,7 +3509,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $baseNetworkExists = $networks->contains(function ($value, $_) use ($baseNetwork) { return $value == $baseNetwork; }); - if (! $baseNetworkExists) { + if (!$baseNetworkExists) { foreach ($baseNetwork as $network) { $topLevel->get('networks')->put($network, [ 'name' => $network, @@ -3535,7 +3541,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $networks_temp = collect(); - if (! $use_network_mode) { + if (!$use_network_mode) { foreach ($networks as $key => $network) { if (gettype($network) === 'string') { // networks: @@ -3566,7 +3572,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $normalEnvironments = $environment->diffKeys($allMagicEnvironments); $normalEnvironments = $normalEnvironments->filter(function ($value, $key) { - return ! str($value)->startsWith('SERVICE_'); + return !str($value)->startsWith('SERVICE_'); }); foreach ($normalEnvironments as $key => $value) { @@ -3586,7 +3592,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int continue; } - if (! $value->startsWith('$')) { + if (!$value->startsWith('$')) { continue; } if ($key->value() === $parsedValue->value()) { @@ -3717,7 +3723,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); } // 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(',')); $urls = $fqdns->map(function ($fqdn) { @@ -3729,7 +3735,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int if ($environment->count() > 0) { $environment = $environment->filter(function ($value, $key) { - return ! str($key)->startsWith('SERVICE_FQDN_'); + return !str($key)->startsWith('SERVICE_FQDN_'); })->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 (str($value)->isEmpty()) { @@ -3756,7 +3762,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) { $shouldGenerateLabelsExactly = $resource->destination->server->settings->generate_exact_labels; $uuid = $resource->uuid; @@ -3849,7 +3855,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int 'restart' => $restart->value(), 'labels' => $serviceLabels, ]); - if (! $use_network_mode) { + if (!$use_network_mode) { $payload['networks'] = $networks_temp; } if ($ports->count() > 0) { @@ -3909,7 +3915,7 @@ function isAssociativeArray($array) $array = $array->toArray(); } - if (! is_array($array)) { + if (!is_array($array)) { throw new \InvalidArgumentException('Input must be an array or a Collection.'); } @@ -4022,7 +4028,7 @@ function instanceSettings() function loadConfigFromGit(string $repository, string $branch, string $base_directory, int $server_id, int $team_id) { $server = Server::find($server_id)->where('team_id', $team_id)->first(); - if (! $server) { + if (!$server) { return; } $uuid = new Cuid2; @@ -4049,7 +4055,7 @@ function loadConfigFromGit(string $repository, string $branch, string $base_dire function loggy($message = null, array $context = []) { - if (! isDev()) { + if (!isDev()) { return; } if (function_exists('ray') && config('app.debug')) { @@ -4084,7 +4090,7 @@ function isEmailRateLimited(string $limiterKey, int $decaySeconds = 3600, ?calla $limiterKey, $maxAttempts = 0, function () use (&$rateLimited, &$limiterKey, $callbackOnSuccess) { - isDev() && loggy('Rate limit not reached for '.$limiterKey); + isDev() && loggy('Rate limit not reached for ' . $limiterKey); $rateLimited = false; if ($callbackOnSuccess) { @@ -4093,8 +4099,8 @@ function isEmailRateLimited(string $limiterKey, int $decaySeconds = 3600, ?calla }, $decaySeconds, ); - if (! $executed) { - isDev() && loggy('Rate limit reached for '.$limiterKey.'. Rate limiter will be disabled for '.$decaySeconds.' seconds.'); + if (!$executed) { + isDev() && loggy('Rate limit reached for ' . $limiterKey . '. Rate limiter will be disabled for ' . $decaySeconds . ' seconds.'); $rateLimited = true; } diff --git a/database/migrations/2024_11_12_213200_add_slack_notifications_to_teams_table.php b/database/migrations/2024_11_12_213200_add_slack_notifications_to_teams_table.php new file mode 100644 index 000000000..a6457269a --- /dev/null +++ b/database/migrations/2024_11_12_213200_add_slack_notifications_to_teams_table.php @@ -0,0 +1,38 @@ +boolean('slack_enabled')->default(false); + $table->string('slack_webhook_url')->nullable(); + $table->boolean('slack_notifications_test')->default(true); + $table->boolean('slack_notifications_deployments')->default(true); + $table->boolean('slack_notifications_status_changes')->default(true); + $table->boolean('slack_notifications_database_backups')->default(true); + $table->boolean('slack_notifications_scheduled_tasks')->default(true); + $table->boolean('slack_notifications_server_disk_usage')->default(true); + }); + } + + 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', + ]); + }); + } +}; diff --git a/public/svgs/heimdall.svg b/public/svgs/heimdall.svg new file mode 100644 index 000000000..6ecfa8457 --- /dev/null +++ b/public/svgs/heimdall.svg @@ -0,0 +1,11 @@ + + + + background + + + + Layer 1 + + + \ No newline at end of file diff --git a/public/svgs/invoiceninja.png b/public/svgs/invoiceninja.png new file mode 100644 index 000000000..5141cd4d1 Binary files /dev/null and b/public/svgs/invoiceninja.png differ diff --git a/public/svgs/kuzzle.png b/public/svgs/kuzzle.png new file mode 100644 index 000000000..a7bf37029 Binary files /dev/null and b/public/svgs/kuzzle.png differ diff --git a/public/svgs/pairdrop.png b/public/svgs/pairdrop.png new file mode 100644 index 000000000..b0e9ee5d0 Binary files /dev/null and b/public/svgs/pairdrop.png differ diff --git a/public/svgs/penpot.svg b/public/svgs/penpot.svg new file mode 100644 index 000000000..6439292bd --- /dev/null +++ b/public/svgs/penpot.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/svgs/whoogle.png b/public/svgs/whoogle.png new file mode 100644 index 000000000..0d89d25f2 Binary files /dev/null and b/public/svgs/whoogle.png differ diff --git a/resources/views/components/notification/navbar.blade.php b/resources/views/components/notification/navbar.blade.php index 0fbbc69a2..c4dbd25af 100644 --- a/resources/views/components/notification/navbar.blade.php +++ b/resources/views/components/notification/navbar.blade.php @@ -15,6 +15,10 @@ href="{{ route('notifications.discord') }}"> + + + - + \ No newline at end of file diff --git a/resources/views/livewire/notifications/slack.blade.php b/resources/views/livewire/notifications/slack.blade.php new file mode 100644 index 000000000..b3685173c --- /dev/null +++ b/resources/views/livewire/notifications/slack.blade.php @@ -0,0 +1,43 @@ +
+ + Notifications | Coolify + + +
+
+

Slack

+ + Save + + @if ($slackEnabled) + + Send Test Notifications + + @endif +
+
+ +
+ + + @if ($slackEnabled) +

Subscribe to events

+
+ @if (isDev()) + + @endif + + + + + +
+ @endif +
\ No newline at end of file diff --git a/resources/views/livewire/server/proxy.blade.php b/resources/views/livewire/server/proxy.blade.php index 25c3f7acd..00e0b04b2 100644 --- a/resources/views/livewire/server/proxy.blade.php +++ b/resources/views/livewire/server/proxy.blade.php @@ -27,6 +27,13 @@ helper="If set, all resources will only have docker container labels for {{ str($server->proxyType())->title() }}.
For applications, labels needs to be regenerated manually.
Resources needs to be restarted." id="server.settings.generate_exact_labels" label="Generate labels only for {{ str($server->proxyType())->title() }}" instantSave /> + + @if ($redirect_enabled) + + @endif @if ($server->proxyType() === ProxyTypes::TRAEFIK->value)

Traefik

@@ -40,8 +47,6 @@ configurations. @endif -
diff --git a/resources/views/livewire/server/proxy/dynamic-configurations.blade.php b/resources/views/livewire/server/proxy/dynamic-configurations.blade.php index ec63f451b..30400ef73 100644 --- a/resources/views/livewire/server/proxy/dynamic-configurations.blade.php +++ b/resources/views/livewire/server/proxy/dynamic-configurations.blade.php @@ -29,7 +29,8 @@ @if (str_replace('|', '.', $fileName) === 'coolify.yaml' || str_replace('|', '.', $fileName) === 'Caddyfile' || str_replace('|', '.', $fileName) === 'coolify.caddy' || - str_replace('|', '.', $fileName) === 'default_redirect_404.caddy') + str_replace('|', '.', $fileName) === 'default_redirect_503.yaml' || + str_replace('|', '.', $fileName) === 'default_redirect_503.caddy')

File: {{ str_replace('|', '.', $fileName) }}

diff --git a/routes/web.php b/routes/web.php index e90bbfac7..d8ba925f2 100644 --- a/routes/web.php +++ b/routes/web.php @@ -13,6 +13,7 @@ use App\Livewire\ForcePasswordReset; use App\Livewire\Notifications\Discord as NotificationDiscord; use App\Livewire\Notifications\Email as NotificationEmail; use App\Livewire\Notifications\Telegram as NotificationTelegram; +use App\Livewire\Notifications\Slack as NotificationSlack; use App\Livewire\Profile\Index as ProfileIndex; use App\Livewire\Project\Application\Configuration as ApplicationConfiguration; use App\Livewire\Project\Application\Deployment\Index as DeploymentIndex; @@ -132,6 +133,7 @@ Route::middleware(['auth', 'verified'])->group(function () { Route::get('/email', NotificationEmail::class)->name('notifications.email'); Route::get('/telegram', NotificationTelegram::class)->name('notifications.telegram'); Route::get('/discord', NotificationDiscord::class)->name('notifications.discord'); + Route::get('/slack', NotificationSlack::class)->name('notifications.slack'); }); Route::prefix('storages')->group(function () { @@ -285,7 +287,7 @@ Route::middleware(['auth'])->group(function () { 'privateKey' => $privateKeyLocation, 'root' => '/', ]); - if (! $disk->exists($filename)) { + if (!$disk->exists($filename)) { return response()->json(['message' => 'Backup not found.'], 404); } @@ -297,7 +299,7 @@ Route::middleware(['auth'])->group(function () { if ($stream === false || is_null($stream)) { abort(500, 'Failed to open stream for the requested file.'); } - while (! feof($stream)) { + while (!feof($stream)) { echo fread($stream, 2048); flush(); } @@ -305,7 +307,7 @@ Route::middleware(['auth'])->group(function () { fclose($stream); }, 200, [ 'Content-Type' => 'application/octet-stream', - 'Content-Disposition' => 'attachment; filename="'.basename($filename).'"', + 'Content-Disposition' => 'attachment; filename="' . basename($filename) . '"', ]); } catch (\Throwable $e) { return response()->json(['message' => $e->getMessage()], 500); diff --git a/scripts/install.sh b/scripts/install.sh index 1a039f64f..3f289438f 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -30,7 +30,7 @@ WARNING_SPACE=false if [ "$TOTAL_SPACE" -lt "$REQUIRED_TOTAL_SPACE" ]; then WARNING_SPACE=true - cat << 'EOF' + cat << EOF WARNING: Insufficient total disk space! Total disk space: ${TOTAL_SPACE}GB @@ -41,7 +41,7 @@ EOF fi if [ "$AVAILABLE_SPACE" -lt "$REQUIRED_AVAILABLE_SPACE" ]; then - cat << 'EOF' + cat << EOF WARNING: Insufficient available disk space! Available disk space: ${AVAILABLE_SPACE}GB @@ -49,7 +49,7 @@ Required available space: ${REQUIRED_AVAILABLE_SPACE}GB ================== EOF - WARNING_SPACE=true +WARNING_SPACE=true fi if [ "$WARNING_SPACE" = true ]; then diff --git a/templates/compose/heimdall.yaml b/templates/compose/heimdall.yaml index 7ae07b296..851f981b5 100644 --- a/templates/compose/heimdall.yaml +++ b/templates/compose/heimdall.yaml @@ -1,6 +1,7 @@ # documentation: https://github.com/linuxserver/Heimdall # slogan: Heimdall is a dashboard for managing and organizing your server applications. # tags: dashboard, server, applications, interface +# logo: svgs/heimdall.svg services: heimdall: diff --git a/templates/compose/invoice-ninja.yaml b/templates/compose/invoice-ninja.yaml index 9a767126b..beb05d983 100644 --- a/templates/compose/invoice-ninja.yaml +++ b/templates/compose/invoice-ninja.yaml @@ -1,6 +1,7 @@ # documentation: https://invoiceninja.github.io/selfhost.html # slogan: The leading open-source invoicing platform # tags: invoicing, billing, accounting, finance, self-hosted +# logo: svgs/invoiceninja.png # port: 9000 services: diff --git a/templates/compose/kuzzle.yaml b/templates/compose/kuzzle.yaml index f0ceaee68..a195cc600 100644 --- a/templates/compose/kuzzle.yaml +++ b/templates/compose/kuzzle.yaml @@ -1,6 +1,7 @@ # documentation: https://kuzzle.io # slogan: Kuzzle is a generic backend offering the basic building blocks common to every application. # tags: backend, api, realtime, websocket, mqtt, rest, sdk, iot, geofencing, low-code +# logo: svgs/kuzzle.png # port: 7512 services: diff --git a/templates/compose/pairdrop.yaml b/templates/compose/pairdrop.yaml index 44bac7000..3e71e8b84 100644 --- a/templates/compose/pairdrop.yaml +++ b/templates/compose/pairdrop.yaml @@ -1,6 +1,7 @@ # documentation: https://pairdrop.net/ # slogan: Pairdrop is a self-hosted file sharing and collaboration platform, offering secure file sharing and collaboration capabilities for efficient teamwork. # tags: file, sharing, collaboration, teamwork +# logo: svgs/pairdrop.png # port: 3000 services: diff --git a/templates/compose/penpot.yaml b/templates/compose/penpot.yaml index 9bc21b398..05b73cdca 100644 --- a/templates/compose/penpot.yaml +++ b/templates/compose/penpot.yaml @@ -1,6 +1,7 @@ # documentation: https://help.penpot.app/technical-guide/getting-started/#install-with-docker # slogan: Penpot is the first Open Source design and prototyping platform for product teams. # tags: penpot,design,prototyping,figma,open,source +# logo: svgs/penpot.svg services: frontend: diff --git a/templates/compose/trigger.yaml b/templates/compose/trigger.yaml index a0007031d..83aa0dfe0 100644 --- a/templates/compose/trigger.yaml +++ b/templates/compose/trigger.yaml @@ -5,7 +5,6 @@ # port: 3000 x-common-env: &common-env - PORT: 3030 REMIX_APP_PORT: 3000 NODE_ENV: production RUNTIME_PLATFORM: docker-compose @@ -118,7 +117,7 @@ services: environment: <<: *common-env PLATFORM_HOST: trigger - PLATFORM_WS_PORT: 3030 + PLATFORM_WS_PORT: 3000 SECURE_CONNECTION: "false" PLATFORM_SECRET: $PROVIDER_SECRET coordinator: @@ -133,7 +132,7 @@ services: environment: <<: *common-env PLATFORM_HOST: trigger - PLATFORM_WS_PORT: 3030 + PLATFORM_WS_PORT: 3000 SECURE_CONNECTION: "false" PLATFORM_SECRET: $COORDINATOR_SECRET healthcheck: diff --git a/templates/compose/whoogle.yaml b/templates/compose/whoogle.yaml index c049dac47..0be303f9a 100644 --- a/templates/compose/whoogle.yaml +++ b/templates/compose/whoogle.yaml @@ -1,6 +1,7 @@ # documentation: https://github.com/benbusby/whoogle-search?tab=readme-ov-file # slogan: Whoogle is a self-hosted, privacy-focused search engine front-end for accessing Google search results without tracking and data collection. # tags: privacy, search engine +# logo: svgs/whoogle.png # port: 5000 services: diff --git a/templates/service-templates.json b/templates/service-templates.json index 1532c1609..34da885d5 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -1125,7 +1125,7 @@ "applications", "interface" ], - "logo": "svgs/default.webp", + "logo": "svgs/heimdall.svg", "minversion": "0.0.0" }, "heyform": { @@ -1251,7 +1251,7 @@ "finance", "self-hosted" ], - "logo": "svgs/default.webp", + "logo": "svgs/invoiceninja.png", "minversion": "0.0.0", "port": "9000" }, @@ -1417,7 +1417,7 @@ "geofencing", "low-code" ], - "logo": "svgs/default.webp", + "logo": "svgs/kuzzle.png", "minversion": "0.0.0", "port": "7512" }, @@ -2112,7 +2112,7 @@ "collaboration", "teamwork" ], - "logo": "svgs/default.webp", + "logo": "svgs/pairdrop.png", "minversion": "0.0.0", "port": "3000" }, @@ -2137,7 +2137,7 @@ "open", "source" ], - "logo": "svgs/default.webp", + "logo": "svgs/penpot.svg", "minversion": "0.0.0" }, "phpmyadmin": { @@ -2245,7 +2245,7 @@ "postiz": { "documentation": "https://docs.postiz.com?utm_source=coolify.io", "slogan": "Open source social media scheduling tool.", - "compose": "c2VydmljZXM6CiAgcG9zdGl6OgogICAgaW1hZ2U6ICdnaGNyLmlvL2dpdHJvb21ocS9wb3N0aXotYXBwOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9QT1NUSVpfNTAwMAogICAgICAtICdNQUlOX1VSTD0ke1NFUlZJQ0VfRlFETl9QT1NUSVp9JwogICAgICAtICdGUk9OVEVORF9VUkw9JHtTRVJWSUNFX0ZRRE5fUE9TVElafScKICAgICAgLSAnTkVYVF9QVUJMSUNfQkFDS0VORF9VUkw9JHtTRVJWSUNFX0ZRRE5fUE9TVElafS9hcGknCiAgICAgIC0gJ0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVFNFQ1JFVH0nCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3Jlc3FsOi8vcG9zdGdyZXM6JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTUUx9QHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1wb3N0aXotZGJ9JwogICAgICAtICdSRURJU19VUkw9cmVkaXM6Ly9kZWZhdWx0OiR7U0VSVklDRV9QQVNTV09SRF9SRURJU31AcmVkaXM6NjM3OScKICAgICAgLSAnQkFDS0VORF9JTlRFUk5BTF9VUkw9aHR0cDovL2xvY2FsaG9zdDozMDAwJwogICAgICAtICdDTE9VREZMQVJFX0FDQ09VTlRfSUQ9JHtDTE9VREZMQVJFX0FDQ09VTlRfSUR9JwogICAgICAtICdDTE9VREZMQVJFX0FDQ0VTU19LRVk9JHtDTE9VREZMQVJFX0FDQ0VTU19LRVl9JwogICAgICAtICdDTE9VREZMQVJFX1NFQ1JFVF9BQ0NFU1NfS0VZPSR7Q0xPVURGTEFSRV9TRUNSRVRfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ0NMT1VERkxBUkVfQlVDS0VUTkFNRT0ke0NMT1VERkxBUkVfQlVDS0VUTkFNRX0nCiAgICAgIC0gJ0NMT1VERkxBUkVfQlVDS0VUX1VSTD0ke0NMT1VERkxBUkVfQlVDS0VUX1VSTH0nCiAgICAgIC0gJ0NMT1VERkxBUkVfUkVHSU9OPSR7Q0xPVURGTEFSRV9SRUdJT059JwogICAgICAtICdTVE9SQUdFX1BST1ZJREVSPSR7U1RPUkFHRV9QUk9WSURFUjotbG9jYWx9JwogICAgICAtICdVUExPQURfRElSRUNUT1JZPSR7VVBMT0FEX0RJUkVDVE9SWTotL3VwbG9hZHN9JwogICAgICAtICdORVhUX1BVQkxJQ19VUExPQURfRElSRUNUT1JZPSR7TkVYVF9QVUJMSUNfVVBMT0FEX0RJUkVDVE9SWTotL3VwbG9hZHN9JwogICAgICAtICdORVhUX1BVQkxJQ19VUExPQURfU1RBVElDX0RJUkVDVE9SWT0ke05FWFRfUFVCTElDX1VQTE9BRF9TVEFUSUNfRElSRUNUT1JZfScKICAgICAgLSAnUkVTRU5EX0FQSV9LRVk9JHtSRVNFTkRfQVBJX0tFWX0nCiAgICAgIC0gJ0VNQUlMX0ZST01fQUREUkVTUz0ke0VNQUlMX0ZST01fQUREUkVTU30nCiAgICAgIC0gJ0VNQUlMX0ZST01fTkFNRT0ke0VNQUlMX0ZST01fTkFNRX0nCiAgICAgIC0gJ1hfQVBJX0tFWT0ke1NFUlZJQ0VfWF9BUEl9JwogICAgICAtICdYX0FQSV9TRUNSRVQ9JHtTRVJWSUNFX1hfU0VDUkVUfScKICAgICAgLSAnTElOS0VESU5fQ0xJRU5UX0lEPSR7U0VSVklDRV9MSU5LRURJTl9JRH0nCiAgICAgIC0gJ0xJTktFRElOX0NMSUVOVF9TRUNSRVQ9JHtTRVJWSUNFX0xJTktFRElOX1NFQ1JFVH0nCiAgICAgIC0gJ1JFRERJVF9DTElFTlRfSUQ9JHtTRVJWSUNFX1JFRERJVF9BUEl9JwogICAgICAtICdSRURESVRfQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfUkVERElUX1NFQ1JFVH0nCiAgICAgIC0gJ0dJVEhVQl9DTElFTlRfSUQ9JHtTRVJWSUNFX0dJVEhVQl9JRH0nCiAgICAgIC0gJ0dJVEhVQl9DTElFTlRfU0VDUkVUPSR7U0VSVklDRV9HSVRIVUJfU0VDUkVUfScKICAgICAgLSAnVEhSRUFEU19BUFBfSUQ9JHtTRVJWSUNFX1RIUkVBRFNfSUR9JwogICAgICAtICdUSFJFQURTX0FQUF9TRUNSRVQ9JHtTRVJWSUNFX1RIUkVBRFNfU0VDUkVUfScKICAgICAgLSAnRkFDRUJPT0tfQVBQX0lEPSR7U0VSVklDRV9GQUNFQk9PS19JRH0nCiAgICAgIC0gJ0ZBQ0VCT09LX0FQUF9TRUNSRVQ9JHtTRVJWSUNFX0ZBQ0VCT09LX1NFQ1JFVH0nCiAgICAgIC0gJ1lPVVRVQkVfQ0xJRU5UX0lEPSR7U0VSVklDRV9ZT1VUVUJFX0lEfScKICAgICAgLSAnWU9VVFVCRV9DTElFTlRfU0VDUkVUPSR7U0VSVklDRV9ZT1VUVUJFX1NFQ1JFVH0nCiAgICAgIC0gJ1RJS1RPS19DTElFTlRfSUQ9JHtTRVJWSUNFX1RJS1RPS19JRH0nCiAgICAgIC0gJ1RJS1RPS19DTElFTlRfU0VDUkVUPSR7U0VSVklDRV9USUtUT0tfU0VDUkVUfScKICAgICAgLSAnUElOVEVSRVNUX0NMSUVOVF9JRD0ke1NFUlZJQ0VfUElOVEVSRVNUX0lEfScKICAgICAgLSAnUElOVEVSRVNUX0NMSUVOVF9TRUNSRVQ9JHtTRVJWSUNFX1BJTlRFUkVTVF9TRUNSRVR9JwogICAgICAtICdEUklCQkJMRV9DTElFTlRfSUQ9JHtTRVJWSUNFX0RSSUJCTEVfSUR9JwogICAgICAtICdEUklCQkJMRV9DTElFTlRfU0VDUkVUPSR7U0VSVklDRV9EUklCQkxFX1NFQ1JFVH0nCiAgICAgIC0gJ0RJU0NPUkRfQ0xJRU5UX0lEPSR7U0VSVklDRV9ESVNDT1JEX0lEfScKICAgICAgLSAnRElTQ09SRF9DTElFTlRfU0VDUkVUPSR7U0VSVklDRV9ESVNDT1JEX1NFQ1JFVH0nCiAgICAgIC0gJ0RJU0NPUkRfQk9UX1RPS0VOX0lEPSR7U0VSVklDRV9ESVNDT1JEX1RPS0VOfScKICAgICAgLSAnU0xBQ0tfSUQ9JHtTRVJWSUNFX1NMQUNLX0lEfScKICAgICAgLSAnU0xBQ0tfU0VDUkVUPSR7U0VSVklDRV9TTEFDS19TRUNSRVR9JwogICAgICAtICdTTEFDS19TSUdOSU5HX1NFQ1JFVD0ke1NMQUNLX1NJR05JTkdfU0VDUkVUfScKICAgICAgLSAnTUFTVE9ET05fQ0xJRU5UX0lEPSR7U0VSVklDRV9NQVNUT0RPTl9JRH0nCiAgICAgIC0gJ01BU1RPRE9OX0NMSUVOVF9TRUNSRVQ9JHtTRVJWSUNFX01BU1RPRE9OX1NFQ1JFVH0nCiAgICAgIC0gJ0JFRUhJSVZFX0FQSV9LRVk9JHtTRVJWSUNFX0JFRUhJSVZFX0tFWX0nCiAgICAgIC0gJ0JFRUhJSVZFX1BVQkxJQ0FUSU9OX0lEPSR7U0VSVklDRV9CRUVISUlWRV9QVUJJRH0nCiAgICAgIC0gJ09QRU5BSV9BUElfS0VZPSR7U0VSVklDRV9PUEVOQUlfS0VZfScKICAgICAgLSAnTkVYVF9QVUJMSUNfRElTQ09SRF9TVVBQT1JUPSR7TkVYVF9QVUJMSUNfRElTQ09SRF9TVVBQT1JUfScKICAgICAgLSAnTkVYVF9QVUJMSUNfUE9MT1ROTz0ke05FWFRfUFVCTElDX1BPTE9UTk99JwogICAgICAtIElTX0dFTkVSQUw9dHJ1ZQogICAgICAtICdOWF9BRERfUExVR0lOUz0ke05YX0FERF9QTFVHSU5TOi1mYWxzZX0nCiAgICAgIC0gJ0ZFRV9BTU9VTlQ9JHtGRUVfQU1PVU5UOi0wLjA1fScKICAgICAgLSAnU1RSSVBFX1BVQkxJU0hBQkxFX0tFWT0ke1NUUklQRV9QVUJMSVNIQUJMRV9LRVl9JwogICAgICAtICdTVFJJUEVfU0VDUkVUX0tFWT0ke1NUUklQRV9TRUNSRVRfS0VZfScKICAgICAgLSAnU1RSSVBFX1NJR05JTkdfS0VZPSR7U1RSSVBFX1NJR05JTkdfS0VZfScKICAgICAgLSAnU1RSSVBFX1NJR05JTkdfS0VZX0NPTk5FQ1Q9JHtTVFJJUEVfU0lHTklOR19LRVlfQ09OTkVDVH0nCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0aXpfY29uZmlnOi9jb25maWcvJwogICAgICAtICdwb3N0aXpfdXBsb2FkczovdXBsb2Fkcy8nCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICd3Z2V0IC1xTy0gaHR0cDovLzEyNy4wLjAuMTo1MDAwLycKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHBvc3RncmVzOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNC41JwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGl6X3Bvc3RncmVzcWxfZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19VU0VSPXBvc3RncmVzCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMfScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1wb3N0aXotZGJ9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCOi1wb3N0aXotZGJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOjcuMicKICAgIGVudmlyb25tZW50OgogICAgICAtICdSRURJU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUkVESVN9JwogICAgY29tbWFuZDogJ3JlZGlzLXNlcnZlciAtLXJlcXVpcmVwYXNzICR7U0VSVklDRV9QQVNTV09SRF9SRURJU30nCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0aXpfcmVkaXNfZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSByZWRpcy1jbGkKICAgICAgICAtICctYScKICAgICAgICAtICcke1NFUlZJQ0VfUEFTU1dPUkRfUkVESVN9JwogICAgICAgIC0gcGluZwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDIwCg==", + "compose": "c2VydmljZXM6CiAgcG9zdGl6OgogICAgaW1hZ2U6ICdnaGNyLmlvL2dpdHJvb21ocS9wb3N0aXotYXBwOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9QT1NUSVpfNTAwMAogICAgICAtICdNQUlOX1VSTD0ke1NFUlZJQ0VfRlFETl9QT1NUSVp9JwogICAgICAtICdGUk9OVEVORF9VUkw9JHtTRVJWSUNFX0ZRRE5fUE9TVElafScKICAgICAgLSAnTkVYVF9QVUJMSUNfQkFDS0VORF9VUkw9JHtTRVJWSUNFX0ZRRE5fUE9TVElafS9hcGknCiAgICAgIC0gJ0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVFNFQ1JFVH0nCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3Jlc3FsOi8vcG9zdGdyZXM6JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTUUx9QHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1wb3N0aXotZGJ9JwogICAgICAtICdSRURJU19VUkw9cmVkaXM6Ly9kZWZhdWx0OiR7U0VSVklDRV9QQVNTV09SRF9SRURJU31AcmVkaXM6NjM3OScKICAgICAgLSAnQkFDS0VORF9JTlRFUk5BTF9VUkw9aHR0cDovL2xvY2FsaG9zdDozMDAwJwogICAgICAtICdDTE9VREZMQVJFX0FDQ09VTlRfSUQ9JHtDTE9VREZMQVJFX0FDQ09VTlRfSUR9JwogICAgICAtICdDTE9VREZMQVJFX0FDQ0VTU19LRVk9JHtDTE9VREZMQVJFX0FDQ0VTU19LRVl9JwogICAgICAtICdDTE9VREZMQVJFX1NFQ1JFVF9BQ0NFU1NfS0VZPSR7Q0xPVURGTEFSRV9TRUNSRVRfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ0NMT1VERkxBUkVfQlVDS0VUTkFNRT0ke0NMT1VERkxBUkVfQlVDS0VUTkFNRX0nCiAgICAgIC0gJ0NMT1VERkxBUkVfQlVDS0VUX1VSTD0ke0NMT1VERkxBUkVfQlVDS0VUX1VSTH0nCiAgICAgIC0gJ0NMT1VERkxBUkVfUkVHSU9OPSR7Q0xPVURGTEFSRV9SRUdJT059JwogICAgICAtICdTVE9SQUdFX1BST1ZJREVSPSR7U1RPUkFHRV9QUk9WSURFUjotbG9jYWx9JwogICAgICAtICdVUExPQURfRElSRUNUT1JZPSR7VVBMT0FEX0RJUkVDVE9SWTotL3VwbG9hZHN9JwogICAgICAtICdORVhUX1BVQkxJQ19VUExPQURfRElSRUNUT1JZPSR7TkVYVF9QVUJMSUNfVVBMT0FEX0RJUkVDVE9SWTotL3VwbG9hZHN9JwogICAgICAtICdORVhUX1BVQkxJQ19VUExPQURfU1RBVElDX0RJUkVDVE9SWT0ke05FWFRfUFVCTElDX1VQTE9BRF9TVEFUSUNfRElSRUNUT1JZfScKICAgICAgLSAnUkVTRU5EX0FQSV9LRVk9JHtSRVNFTkRfQVBJX0tFWX0nCiAgICAgIC0gJ0VNQUlMX0ZST01fQUREUkVTUz0ke0VNQUlMX0ZST01fQUREUkVTU30nCiAgICAgIC0gJ0VNQUlMX0ZST01fTkFNRT0ke0VNQUlMX0ZST01fTkFNRX0nCiAgICAgIC0gJ0VNQUlMX1BST1ZJREVSPSR7RU1BSUxfUFJPVklERVJ9JwogICAgICAtICdYX0FQSV9LRVk9JHtTRVJWSUNFX1hfQVBJfScKICAgICAgLSAnWF9BUElfU0VDUkVUPSR7U0VSVklDRV9YX1NFQ1JFVH0nCiAgICAgIC0gJ0xJTktFRElOX0NMSUVOVF9JRD0ke1NFUlZJQ0VfTElOS0VESU5fSUR9JwogICAgICAtICdMSU5LRURJTl9DTElFTlRfU0VDUkVUPSR7U0VSVklDRV9MSU5LRURJTl9TRUNSRVR9JwogICAgICAtICdSRURESVRfQ0xJRU5UX0lEPSR7U0VSVklDRV9SRURESVRfQVBJfScKICAgICAgLSAnUkVERElUX0NMSUVOVF9TRUNSRVQ9JHtTRVJWSUNFX1JFRERJVF9TRUNSRVR9JwogICAgICAtICdHSVRIVUJfQ0xJRU5UX0lEPSR7U0VSVklDRV9HSVRIVUJfSUR9JwogICAgICAtICdHSVRIVUJfQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfR0lUSFVCX1NFQ1JFVH0nCiAgICAgIC0gJ1RIUkVBRFNfQVBQX0lEPSR7U0VSVklDRV9USFJFQURTX0lEfScKICAgICAgLSAnVEhSRUFEU19BUFBfU0VDUkVUPSR7U0VSVklDRV9USFJFQURTX1NFQ1JFVH0nCiAgICAgIC0gJ0ZBQ0VCT09LX0FQUF9JRD0ke1NFUlZJQ0VfRkFDRUJPT0tfSUR9JwogICAgICAtICdGQUNFQk9PS19BUFBfU0VDUkVUPSR7U0VSVklDRV9GQUNFQk9PS19TRUNSRVR9JwogICAgICAtICdZT1VUVUJFX0NMSUVOVF9JRD0ke1NFUlZJQ0VfWU9VVFVCRV9JRH0nCiAgICAgIC0gJ1lPVVRVQkVfQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfWU9VVFVCRV9TRUNSRVR9JwogICAgICAtICdUSUtUT0tfQ0xJRU5UX0lEPSR7U0VSVklDRV9USUtUT0tfSUR9JwogICAgICAtICdUSUtUT0tfQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfVElLVE9LX1NFQ1JFVH0nCiAgICAgIC0gJ1BJTlRFUkVTVF9DTElFTlRfSUQ9JHtTRVJWSUNFX1BJTlRFUkVTVF9JRH0nCiAgICAgIC0gJ1BJTlRFUkVTVF9DTElFTlRfU0VDUkVUPSR7U0VSVklDRV9QSU5URVJFU1RfU0VDUkVUfScKICAgICAgLSAnRFJJQkJCTEVfQ0xJRU5UX0lEPSR7U0VSVklDRV9EUklCQkxFX0lEfScKICAgICAgLSAnRFJJQkJCTEVfQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfRFJJQkJMRV9TRUNSRVR9JwogICAgICAtICdESVNDT1JEX0NMSUVOVF9JRD0ke1NFUlZJQ0VfRElTQ09SRF9JRH0nCiAgICAgIC0gJ0RJU0NPUkRfQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfRElTQ09SRF9TRUNSRVR9JwogICAgICAtICdESVNDT1JEX0JPVF9UT0tFTl9JRD0ke1NFUlZJQ0VfRElTQ09SRF9UT0tFTn0nCiAgICAgIC0gJ1NMQUNLX0lEPSR7U0VSVklDRV9TTEFDS19JRH0nCiAgICAgIC0gJ1NMQUNLX1NFQ1JFVD0ke1NFUlZJQ0VfU0xBQ0tfU0VDUkVUfScKICAgICAgLSAnU0xBQ0tfU0lHTklOR19TRUNSRVQ9JHtTTEFDS19TSUdOSU5HX1NFQ1JFVH0nCiAgICAgIC0gJ01BU1RPRE9OX0NMSUVOVF9JRD0ke1NFUlZJQ0VfTUFTVE9ET05fSUR9JwogICAgICAtICdNQVNUT0RPTl9DTElFTlRfU0VDUkVUPSR7U0VSVklDRV9NQVNUT0RPTl9TRUNSRVR9JwogICAgICAtICdCRUVISUlWRV9BUElfS0VZPSR7U0VSVklDRV9CRUVISUlWRV9LRVl9JwogICAgICAtICdCRUVISUlWRV9QVUJMSUNBVElPTl9JRD0ke1NFUlZJQ0VfQkVFSElJVkVfUFVCSUR9JwogICAgICAtICdPUEVOQUlfQVBJX0tFWT0ke1NFUlZJQ0VfT1BFTkFJX0tFWX0nCiAgICAgIC0gJ05FWFRfUFVCTElDX0RJU0NPUkRfU1VQUE9SVD0ke05FWFRfUFVCTElDX0RJU0NPUkRfU1VQUE9SVH0nCiAgICAgIC0gJ05FWFRfUFVCTElDX1BPTE9UTk89JHtORVhUX1BVQkxJQ19QT0xPVE5PfScKICAgICAgLSBJU19HRU5FUkFMPXRydWUKICAgICAgLSAnTlhfQUREX1BMVUdJTlM9JHtOWF9BRERfUExVR0lOUzotZmFsc2V9JwogICAgICAtICdGRUVfQU1PVU5UPSR7RkVFX0FNT1VOVDotMC4wNX0nCiAgICAgIC0gJ1NUUklQRV9QVUJMSVNIQUJMRV9LRVk9JHtTVFJJUEVfUFVCTElTSEFCTEVfS0VZfScKICAgICAgLSAnU1RSSVBFX1NFQ1JFVF9LRVk9JHtTVFJJUEVfU0VDUkVUX0tFWX0nCiAgICAgIC0gJ1NUUklQRV9TSUdOSU5HX0tFWT0ke1NUUklQRV9TSUdOSU5HX0tFWX0nCiAgICAgIC0gJ1NUUklQRV9TSUdOSU5HX0tFWV9DT05ORUNUPSR7U1RSSVBFX1NJR05JTkdfS0VZX0NPTk5FQ1R9JwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGl6X2NvbmZpZzovY29uZmlnLycKICAgICAgLSAncG9zdGl6X3VwbG9hZHM6L3VwbG9hZHMvJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnd2dldCAtcU8tIGh0dHA6Ly8xMjcuMC4wLjE6NTAwMC8nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBwb3N0Z3JlczoKICAgIGltYWdlOiAncG9zdGdyZXM6MTQuNScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3Rpel9wb3N0Z3Jlc3FsX2RhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfVVNFUj1wb3N0Z3JlcwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNRTF9EQVRBQkFTRTotcG9zdGl6LWRifScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQjotcG9zdGl6LWRifScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHJlZGlzOgogICAgaW1hZ2U6ICdyZWRpczo3LjInCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUkVESVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1JFRElTfScKICAgIGNvbW1hbmQ6ICdyZWRpcy1zZXJ2ZXIgLS1yZXF1aXJlcGFzcyAke1NFUlZJQ0VfUEFTU1dPUkRfUkVESVN9JwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGl6X3JlZGlzX2RhdGE6L2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSAnLWEnCiAgICAgICAgLSAnJHtTRVJWSUNFX1BBU1NXT1JEX1JFRElTfScKICAgICAgICAtIHBpbmcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAyMAo=", "tags": [ "post everywhere", "social media", @@ -2696,7 +2696,7 @@ "trigger": { "documentation": "https://trigger.dev?utm_source=coolify.io", "slogan": "The open source Background Jobs framework for TypeScript", - "compose": "eC1jb21tb24tZW52OgogIFBPUlQ6IDMwMzAKICBSRU1JWF9BUFBfUE9SVDogMzAwMAogIE5PREVfRU5WOiBwcm9kdWN0aW9uCiAgUlVOVElNRV9QTEFURk9STTogZG9ja2VyLWNvbXBvc2UKICBWM19FTkFCTEVEOiB0cnVlCiAgSU5URVJOQUxfT1RFTF9UUkFDRV9ESVNBQkxFRDogMQogIElOVEVSTkFMX09URUxfVFJBQ0VfTE9HR0lOR19FTkFCTEVEOiAwCiAgUE9TVEdSRVNfVVNFUjogJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogIFBPU1RHUkVTX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogIFBPU1RHUkVTX0RCOiAnJHtQT1NUR1JFU19EQjotdHJpZ2dlcn0nCiAgTUFHSUNfTElOS19TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X01BR0lDCiAgU0VTU0lPTl9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X1NFU1NJT04KICBFTkNSWVBUSU9OX0tFWTogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfRU5DUllQVElPTgogIFBST1ZJREVSX1NFQ1JFVDogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfUFJPVklERVIKICBDT09SRElOQVRPUl9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X0NPT1JESU5BVE9SCiAgREFUQUJBU0VfSE9TVDogJ3Bvc3RncmVzcWw6NTQzMicKICBEQVRBQkFTRV9VUkw6ICdwb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcG9zdGdyZXNxbDo1NDMyLyRQT1NUR1JFU19EQj9zc2xtb2RlPWRpc2FibGUnCiAgRElSRUNUX1VSTDogJ3Bvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0Bwb3N0Z3Jlc3FsOjU0MzIvJFBPU1RHUkVTX0RCP3NzbG1vZGU9ZGlzYWJsZScKICBSRURJU19IT1NUOiByZWRpcwogIFJFRElTX1BPUlQ6IDYzNzkKICBSRURJU19UTFNfRElTQUJMRUQ6IHRydWUKICBDT09SRElOQVRPUl9IT1NUOiAxMjcuMC4wLjEKICBDT09SRElOQVRPUl9QT1JUOiA5MDIwCiAgV0hJVEVMSVNURURfRU1BSUxTOiAnJwogIEFETUlOX0VNQUlMUzogJycKICBERUZBVUxUX09SR19FWEVDVVRJT05fQ09OQ1VSUkVOQ1lfTElNSVQ6IDMwMAogIERFRkFVTFRfRU5WX0VYRUNVVElPTl9DT05DVVJSRU5DWV9MSU1JVDogMTAwCiAgREVQTE9ZX1JFR0lTVFJZX0hPU1Q6IGRvY2tlci5pbwogIERFUExPWV9SRUdJU1RSWV9OQU1FU1BBQ0U6IHRyaWdnZXIKICBSRUdJU1RSWV9IT1NUOiAnJHtERVBMT1lfUkVHSVNUUllfSE9TVH0nCiAgUkVHSVNUUllfTkFNRVNQQUNFOiAnJHtERVBMT1lfUkVHSVNUUllfTkFNRVNQQUNFfScKICBBVVRIX0dJVEhVQl9DTElFTlRfSUQ6ICcke0FVVEhfR0lUSFVCX0NMSUVOVF9JRH0nCiAgQVVUSF9HSVRIVUJfQ0xJRU5UX1NFQ1JFVDogJyR7QVVUSF9HSVRIVUJfQ0xJRU5UX1NFQ1JFVH0nCiAgUkVTRU5EX0FQSV9LRVk6ICcke1JFU0VORF9BUElfS0VZfScKICBGUk9NX0VNQUlMOiAnJHtGUk9NX0VNQUlMfScKICBSRVBMWV9UT19FTUFJTDogJyR7UkVQTFlfVE9fRU1BSUx9JwogIExPR0lOX09SSUdJTjogJFNFUlZJQ0VfRlFETl9UUklHR0VSXzMwMDAKICBBUFBfT1JJR0lOOiAkU0VSVklDRV9GUUROX1RSSUdHRVJfMzAwMAogIERFVl9PVEVMX0VYUE9SVEVSX09UTFBfRU5EUE9JTlQ6ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwL290ZWwKICBPVEVMX0VYUE9SVEVSX09UTFBfRU5EUE9JTlQ6ICdodHRwOi8vdHJpZ2dlcjozMDQwL290ZWwnCiAgRUxFQ1RSSUNfT1JJR0lOOiAnaHR0cDovL2VsZWN0cmljOjMwMDAnCnNlcnZpY2VzOgogIHRyaWdnZXI6CiAgICBpbWFnZTogJ2doY3IuaW8vdHJpZ2dlcmRvdGRldi90cmlnZ2VyLmRldjp2MycKICAgIGVudmlyb25tZW50OgogICAgICBTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwOiAnJwogICAgICBQT1JUOiAzMDMwCiAgICAgIFJFTUlYX0FQUF9QT1JUOiAzMDAwCiAgICAgIE5PREVfRU5WOiBwcm9kdWN0aW9uCiAgICAgIFJVTlRJTUVfUExBVEZPUk06IGRvY2tlci1jb21wb3NlCiAgICAgIFYzX0VOQUJMRUQ6IHRydWUKICAgICAgSU5URVJOQUxfT1RFTF9UUkFDRV9ESVNBQkxFRDogMQogICAgICBJTlRFUk5BTF9PVEVMX1RSQUNFX0xPR0dJTkdfRU5BQkxFRDogMAogICAgICBQT1NUR1JFU19VU0VSOiAkU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIFBPU1RHUkVTX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICBQT1NUR1JFU19EQjogJyR7UE9TVEdSRVNfREI6LXRyaWdnZXJ9JwogICAgICBNQUdJQ19MSU5LX1NFQ1JFVDogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfTUFHSUMKICAgICAgU0VTU0lPTl9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X1NFU1NJT04KICAgICAgRU5DUllQVElPTl9LRVk6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X0VOQ1JZUFRJT04KICAgICAgUFJPVklERVJfU0VDUkVUOiAkU0VSVklDRV9QQVNTV09SRF82NF9QUk9WSURFUgogICAgICBDT09SRElOQVRPUl9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X0NPT1JESU5BVE9SCiAgICAgIERBVEFCQVNFX0hPU1Q6ICdwb3N0Z3Jlc3FsOjU0MzInCiAgICAgIERBVEFCQVNFX1VSTDogJ3Bvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0Bwb3N0Z3Jlc3FsOjU0MzIvJFBPU1RHUkVTX0RCP3NzbG1vZGU9ZGlzYWJsZScKICAgICAgRElSRUNUX1VSTDogJ3Bvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0Bwb3N0Z3Jlc3FsOjU0MzIvJFBPU1RHUkVTX0RCP3NzbG1vZGU9ZGlzYWJsZScKICAgICAgUkVESVNfSE9TVDogcmVkaXMKICAgICAgUkVESVNfUE9SVDogNjM3OQogICAgICBSRURJU19UTFNfRElTQUJMRUQ6IHRydWUKICAgICAgQ09PUkRJTkFUT1JfSE9TVDogMTI3LjAuMC4xCiAgICAgIENPT1JESU5BVE9SX1BPUlQ6IDkwMjAKICAgICAgV0hJVEVMSVNURURfRU1BSUxTOiAnJwogICAgICBBRE1JTl9FTUFJTFM6ICcnCiAgICAgIERFRkFVTFRfT1JHX0VYRUNVVElPTl9DT05DVVJSRU5DWV9MSU1JVDogMzAwCiAgICAgIERFRkFVTFRfRU5WX0VYRUNVVElPTl9DT05DVVJSRU5DWV9MSU1JVDogMTAwCiAgICAgIERFUExPWV9SRUdJU1RSWV9IT1NUOiBkb2NrZXIuaW8KICAgICAgREVQTE9ZX1JFR0lTVFJZX05BTUVTUEFDRTogdHJpZ2dlcgogICAgICBSRUdJU1RSWV9IT1NUOiAnJHtERVBMT1lfUkVHSVNUUllfSE9TVH0nCiAgICAgIFJFR0lTVFJZX05BTUVTUEFDRTogJyR7REVQTE9ZX1JFR0lTVFJZX05BTUVTUEFDRX0nCiAgICAgIEFVVEhfR0lUSFVCX0NMSUVOVF9JRDogJyR7QVVUSF9HSVRIVUJfQ0xJRU5UX0lEfScKICAgICAgQVVUSF9HSVRIVUJfQ0xJRU5UX1NFQ1JFVDogJyR7QVVUSF9HSVRIVUJfQ0xJRU5UX1NFQ1JFVH0nCiAgICAgIFJFU0VORF9BUElfS0VZOiAnJHtSRVNFTkRfQVBJX0tFWX0nCiAgICAgIEZST01fRU1BSUw6ICcke0ZST01fRU1BSUx9JwogICAgICBSRVBMWV9UT19FTUFJTDogJyR7UkVQTFlfVE9fRU1BSUx9JwogICAgICBMT0dJTl9PUklHSU46ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwCiAgICAgIEFQUF9PUklHSU46ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwCiAgICAgIERFVl9PVEVMX0VYUE9SVEVSX09UTFBfRU5EUE9JTlQ6ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwL290ZWwKICAgICAgT1RFTF9FWFBPUlRFUl9PVExQX0VORFBPSU5UOiAnaHR0cDovL3RyaWdnZXI6MzA0MC9vdGVsJwogICAgICBFTEVDVFJJQ19PUklHSU46ICdodHRwOi8vZWxlY3RyaWM6MzAwMCcKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgZWxlY3RyaWM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OiAidGltZW91dCAxMHMgYmFzaCAtYyAnOj4gL2Rldi90Y3AvMTI3LjAuMC4xLzMwMDAnIHx8IGV4aXQgMSIKICAgICAgaW50ZXJ2YWw6IDEwcwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiA1CiAgZWxlY3RyaWM6CiAgICBpbWFnZTogZWxlY3RyaWNzcWwvZWxlY3RyaWMKICAgIGVudmlyb25tZW50OgogICAgICBQT1JUOiAzMDMwCiAgICAgIFJFTUlYX0FQUF9QT1JUOiAzMDAwCiAgICAgIE5PREVfRU5WOiBwcm9kdWN0aW9uCiAgICAgIFJVTlRJTUVfUExBVEZPUk06IGRvY2tlci1jb21wb3NlCiAgICAgIFYzX0VOQUJMRUQ6IHRydWUKICAgICAgSU5URVJOQUxfT1RFTF9UUkFDRV9ESVNBQkxFRDogMQogICAgICBJTlRFUk5BTF9PVEVMX1RSQUNFX0xPR0dJTkdfRU5BQkxFRDogMAogICAgICBQT1NUR1JFU19VU0VSOiAkU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIFBPU1RHUkVTX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICBQT1NUR1JFU19EQjogJyR7UE9TVEdSRVNfREI6LXRyaWdnZXJ9JwogICAgICBNQUdJQ19MSU5LX1NFQ1JFVDogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfTUFHSUMKICAgICAgU0VTU0lPTl9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X1NFU1NJT04KICAgICAgRU5DUllQVElPTl9LRVk6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X0VOQ1JZUFRJT04KICAgICAgUFJPVklERVJfU0VDUkVUOiAkU0VSVklDRV9QQVNTV09SRF82NF9QUk9WSURFUgogICAgICBDT09SRElOQVRPUl9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X0NPT1JESU5BVE9SCiAgICAgIERBVEFCQVNFX0hPU1Q6ICdwb3N0Z3Jlc3FsOjU0MzInCiAgICAgIERBVEFCQVNFX1VSTDogJ3Bvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0Bwb3N0Z3Jlc3FsOjU0MzIvJFBPU1RHUkVTX0RCP3NzbG1vZGU9ZGlzYWJsZScKICAgICAgRElSRUNUX1VSTDogJ3Bvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0Bwb3N0Z3Jlc3FsOjU0MzIvJFBPU1RHUkVTX0RCP3NzbG1vZGU9ZGlzYWJsZScKICAgICAgUkVESVNfSE9TVDogcmVkaXMKICAgICAgUkVESVNfUE9SVDogNjM3OQogICAgICBSRURJU19UTFNfRElTQUJMRUQ6IHRydWUKICAgICAgQ09PUkRJTkFUT1JfSE9TVDogMTI3LjAuMC4xCiAgICAgIENPT1JESU5BVE9SX1BPUlQ6IDkwMjAKICAgICAgV0hJVEVMSVNURURfRU1BSUxTOiAnJwogICAgICBBRE1JTl9FTUFJTFM6ICcnCiAgICAgIERFRkFVTFRfT1JHX0VYRUNVVElPTl9DT05DVVJSRU5DWV9MSU1JVDogMzAwCiAgICAgIERFRkFVTFRfRU5WX0VYRUNVVElPTl9DT05DVVJSRU5DWV9MSU1JVDogMTAwCiAgICAgIERFUExPWV9SRUdJU1RSWV9IT1NUOiBkb2NrZXIuaW8KICAgICAgREVQTE9ZX1JFR0lTVFJZX05BTUVTUEFDRTogdHJpZ2dlcgogICAgICBSRUdJU1RSWV9IT1NUOiAnJHtERVBMT1lfUkVHSVNUUllfSE9TVH0nCiAgICAgIFJFR0lTVFJZX05BTUVTUEFDRTogJyR7REVQTE9ZX1JFR0lTVFJZX05BTUVTUEFDRX0nCiAgICAgIEFVVEhfR0lUSFVCX0NMSUVOVF9JRDogJyR7QVVUSF9HSVRIVUJfQ0xJRU5UX0lEfScKICAgICAgQVVUSF9HSVRIVUJfQ0xJRU5UX1NFQ1JFVDogJyR7QVVUSF9HSVRIVUJfQ0xJRU5UX1NFQ1JFVH0nCiAgICAgIFJFU0VORF9BUElfS0VZOiAnJHtSRVNFTkRfQVBJX0tFWX0nCiAgICAgIEZST01fRU1BSUw6ICcke0ZST01fRU1BSUx9JwogICAgICBSRVBMWV9UT19FTUFJTDogJyR7UkVQTFlfVE9fRU1BSUx9JwogICAgICBMT0dJTl9PUklHSU46ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwCiAgICAgIEFQUF9PUklHSU46ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwCiAgICAgIERFVl9PVEVMX0VYUE9SVEVSX09UTFBfRU5EUE9JTlQ6ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwL290ZWwKICAgICAgT1RFTF9FWFBPUlRFUl9PVExQX0VORFBPSU5UOiAnaHR0cDovL3RyaWdnZXI6MzA0MC9vdGVsJwogICAgICBFTEVDVFJJQ19PUklHSU46ICdodHRwOi8vZWxlY3RyaWM6MzAwMCcKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSBwd2QKICByZWRpczoKICAgIGltYWdlOiAncmVkaXM6NycKICAgIGVudmlyb25tZW50OgogICAgICAtIEFMTE9XX0VNUFRZX1BBU1NXT1JEPXllcwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdyZWRpcy1jbGkgLWggbG9jYWxob3N0IC1wIDYzNzkgcGluZycKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDMKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3JlZGlzLWRhdGE6L2RhdGEnCiAgcG9zdGdyZXNxbDoKICAgIGltYWdlOiAncG9zdGdyZXM6MTYtYWxwaW5lJwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXNxbC1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICBQT1JUOiAzMDMwCiAgICAgIFJFTUlYX0FQUF9QT1JUOiAzMDAwCiAgICAgIE5PREVfRU5WOiBwcm9kdWN0aW9uCiAgICAgIFJVTlRJTUVfUExBVEZPUk06IGRvY2tlci1jb21wb3NlCiAgICAgIFYzX0VOQUJMRUQ6IHRydWUKICAgICAgSU5URVJOQUxfT1RFTF9UUkFDRV9ESVNBQkxFRDogMQogICAgICBJTlRFUk5BTF9PVEVMX1RSQUNFX0xPR0dJTkdfRU5BQkxFRDogMAogICAgICBQT1NUR1JFU19VU0VSOiAkU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIFBPU1RHUkVTX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICBQT1NUR1JFU19EQjogJyR7UE9TVEdSRVNfREI6LXRyaWdnZXJ9JwogICAgICBNQUdJQ19MSU5LX1NFQ1JFVDogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfTUFHSUMKICAgICAgU0VTU0lPTl9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X1NFU1NJT04KICAgICAgRU5DUllQVElPTl9LRVk6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X0VOQ1JZUFRJT04KICAgICAgUFJPVklERVJfU0VDUkVUOiAkU0VSVklDRV9QQVNTV09SRF82NF9QUk9WSURFUgogICAgICBDT09SRElOQVRPUl9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X0NPT1JESU5BVE9SCiAgICAgIERBVEFCQVNFX0hPU1Q6ICdwb3N0Z3Jlc3FsOjU0MzInCiAgICAgIERBVEFCQVNFX1VSTDogJ3Bvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0Bwb3N0Z3Jlc3FsOjU0MzIvJFBPU1RHUkVTX0RCP3NzbG1vZGU9ZGlzYWJsZScKICAgICAgRElSRUNUX1VSTDogJ3Bvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0Bwb3N0Z3Jlc3FsOjU0MzIvJFBPU1RHUkVTX0RCP3NzbG1vZGU9ZGlzYWJsZScKICAgICAgUkVESVNfSE9TVDogcmVkaXMKICAgICAgUkVESVNfUE9SVDogNjM3OQogICAgICBSRURJU19UTFNfRElTQUJMRUQ6IHRydWUKICAgICAgQ09PUkRJTkFUT1JfSE9TVDogMTI3LjAuMC4xCiAgICAgIENPT1JESU5BVE9SX1BPUlQ6IDkwMjAKICAgICAgV0hJVEVMSVNURURfRU1BSUxTOiAnJwogICAgICBBRE1JTl9FTUFJTFM6ICcnCiAgICAgIERFRkFVTFRfT1JHX0VYRUNVVElPTl9DT05DVVJSRU5DWV9MSU1JVDogMzAwCiAgICAgIERFRkFVTFRfRU5WX0VYRUNVVElPTl9DT05DVVJSRU5DWV9MSU1JVDogMTAwCiAgICAgIERFUExPWV9SRUdJU1RSWV9IT1NUOiBkb2NrZXIuaW8KICAgICAgREVQTE9ZX1JFR0lTVFJZX05BTUVTUEFDRTogdHJpZ2dlcgogICAgICBSRUdJU1RSWV9IT1NUOiAnJHtERVBMT1lfUkVHSVNUUllfSE9TVH0nCiAgICAgIFJFR0lTVFJZX05BTUVTUEFDRTogJyR7REVQTE9ZX1JFR0lTVFJZX05BTUVTUEFDRX0nCiAgICAgIEFVVEhfR0lUSFVCX0NMSUVOVF9JRDogJyR7QVVUSF9HSVRIVUJfQ0xJRU5UX0lEfScKICAgICAgQVVUSF9HSVRIVUJfQ0xJRU5UX1NFQ1JFVDogJyR7QVVUSF9HSVRIVUJfQ0xJRU5UX1NFQ1JFVH0nCiAgICAgIFJFU0VORF9BUElfS0VZOiAnJHtSRVNFTkRfQVBJX0tFWX0nCiAgICAgIEZST01fRU1BSUw6ICcke0ZST01fRU1BSUx9JwogICAgICBSRVBMWV9UT19FTUFJTDogJyR7UkVQTFlfVE9fRU1BSUx9JwogICAgICBMT0dJTl9PUklHSU46ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwCiAgICAgIEFQUF9PUklHSU46ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwCiAgICAgIERFVl9PVEVMX0VYUE9SVEVSX09UTFBfRU5EUE9JTlQ6ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwL290ZWwKICAgICAgT1RFTF9FWFBPUlRFUl9PVExQX0VORFBPSU5UOiAnaHR0cDovL3RyaWdnZXI6MzA0MC9vdGVsJwogICAgICBFTEVDVFJJQ19PUklHSU46ICdodHRwOi8vZWxlY3RyaWM6MzAwMCcKICAgIGNvbW1hbmQ6CiAgICAgIC0gJy1jJwogICAgICAtIHdhbF9sZXZlbD1sb2dpY2FsCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgZG9ja2VyLXByb3ZpZGVyOgogICAgaW1hZ2U6ICdnaGNyLmlvL3RyaWdnZXJkb3RkZXYvcHJvdmlkZXIvZG9ja2VyOnYzJwogICAgcGxhdGZvcm06IGxpbnV4L2FtZDY0CiAgICB2b2x1bWVzOgogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jaycKICAgIHVzZXI6IHJvb3QKICAgIGRlcGVuZHNfb246CiAgICAgIHRyaWdnZXI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICBQT1JUOiAzMDMwCiAgICAgIFJFTUlYX0FQUF9QT1JUOiAzMDAwCiAgICAgIE5PREVfRU5WOiBwcm9kdWN0aW9uCiAgICAgIFJVTlRJTUVfUExBVEZPUk06IGRvY2tlci1jb21wb3NlCiAgICAgIFYzX0VOQUJMRUQ6IHRydWUKICAgICAgSU5URVJOQUxfT1RFTF9UUkFDRV9ESVNBQkxFRDogMQogICAgICBJTlRFUk5BTF9PVEVMX1RSQUNFX0xPR0dJTkdfRU5BQkxFRDogMAogICAgICBQT1NUR1JFU19VU0VSOiAkU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIFBPU1RHUkVTX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICBQT1NUR1JFU19EQjogJyR7UE9TVEdSRVNfREI6LXRyaWdnZXJ9JwogICAgICBNQUdJQ19MSU5LX1NFQ1JFVDogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfTUFHSUMKICAgICAgU0VTU0lPTl9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X1NFU1NJT04KICAgICAgRU5DUllQVElPTl9LRVk6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X0VOQ1JZUFRJT04KICAgICAgUFJPVklERVJfU0VDUkVUOiAkU0VSVklDRV9QQVNTV09SRF82NF9QUk9WSURFUgogICAgICBDT09SRElOQVRPUl9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X0NPT1JESU5BVE9SCiAgICAgIERBVEFCQVNFX0hPU1Q6ICdwb3N0Z3Jlc3FsOjU0MzInCiAgICAgIERBVEFCQVNFX1VSTDogJ3Bvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0Bwb3N0Z3Jlc3FsOjU0MzIvJFBPU1RHUkVTX0RCP3NzbG1vZGU9ZGlzYWJsZScKICAgICAgRElSRUNUX1VSTDogJ3Bvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0Bwb3N0Z3Jlc3FsOjU0MzIvJFBPU1RHUkVTX0RCP3NzbG1vZGU9ZGlzYWJsZScKICAgICAgUkVESVNfSE9TVDogcmVkaXMKICAgICAgUkVESVNfUE9SVDogNjM3OQogICAgICBSRURJU19UTFNfRElTQUJMRUQ6IHRydWUKICAgICAgQ09PUkRJTkFUT1JfSE9TVDogMTI3LjAuMC4xCiAgICAgIENPT1JESU5BVE9SX1BPUlQ6IDkwMjAKICAgICAgV0hJVEVMSVNURURfRU1BSUxTOiAnJwogICAgICBBRE1JTl9FTUFJTFM6ICcnCiAgICAgIERFRkFVTFRfT1JHX0VYRUNVVElPTl9DT05DVVJSRU5DWV9MSU1JVDogMzAwCiAgICAgIERFRkFVTFRfRU5WX0VYRUNVVElPTl9DT05DVVJSRU5DWV9MSU1JVDogMTAwCiAgICAgIERFUExPWV9SRUdJU1RSWV9IT1NUOiBkb2NrZXIuaW8KICAgICAgREVQTE9ZX1JFR0lTVFJZX05BTUVTUEFDRTogdHJpZ2dlcgogICAgICBSRUdJU1RSWV9IT1NUOiAnJHtERVBMT1lfUkVHSVNUUllfSE9TVH0nCiAgICAgIFJFR0lTVFJZX05BTUVTUEFDRTogJyR7REVQTE9ZX1JFR0lTVFJZX05BTUVTUEFDRX0nCiAgICAgIEFVVEhfR0lUSFVCX0NMSUVOVF9JRDogJyR7QVVUSF9HSVRIVUJfQ0xJRU5UX0lEfScKICAgICAgQVVUSF9HSVRIVUJfQ0xJRU5UX1NFQ1JFVDogJyR7QVVUSF9HSVRIVUJfQ0xJRU5UX1NFQ1JFVH0nCiAgICAgIFJFU0VORF9BUElfS0VZOiAnJHtSRVNFTkRfQVBJX0tFWX0nCiAgICAgIEZST01fRU1BSUw6ICcke0ZST01fRU1BSUx9JwogICAgICBSRVBMWV9UT19FTUFJTDogJyR7UkVQTFlfVE9fRU1BSUx9JwogICAgICBMT0dJTl9PUklHSU46ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwCiAgICAgIEFQUF9PUklHSU46ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwCiAgICAgIERFVl9PVEVMX0VYUE9SVEVSX09UTFBfRU5EUE9JTlQ6ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwL290ZWwKICAgICAgT1RFTF9FWFBPUlRFUl9PVExQX0VORFBPSU5UOiAnaHR0cDovL3RyaWdnZXI6MzA0MC9vdGVsJwogICAgICBFTEVDVFJJQ19PUklHSU46ICdodHRwOi8vZWxlY3RyaWM6MzAwMCcKICAgICAgUExBVEZPUk1fSE9TVDogdHJpZ2dlcgogICAgICBQTEFURk9STV9XU19QT1JUOiAzMDMwCiAgICAgIFNFQ1VSRV9DT05ORUNUSU9OOiAnZmFsc2UnCiAgICAgIFBMQVRGT1JNX1NFQ1JFVDogJFBST1ZJREVSX1NFQ1JFVAogIGNvb3JkaW5hdG9yOgogICAgaW1hZ2U6ICdnaGNyLmlvL3RyaWdnZXJkb3RkZXYvY29vcmRpbmF0b3I6djMnCiAgICBwbGF0Zm9ybTogbGludXgvYW1kNjQKICAgIHZvbHVtZXM6CiAgICAgIC0gJy92YXIvcnVuL2RvY2tlci5zb2NrOi92YXIvcnVuL2RvY2tlci5zb2NrJwogICAgdXNlcjogcm9vdAogICAgZGVwZW5kc19vbjoKICAgICAgdHJpZ2dlcjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIFBPUlQ6IDMwMzAKICAgICAgUkVNSVhfQVBQX1BPUlQ6IDMwMDAKICAgICAgTk9ERV9FTlY6IHByb2R1Y3Rpb24KICAgICAgUlVOVElNRV9QTEFURk9STTogZG9ja2VyLWNvbXBvc2UKICAgICAgVjNfRU5BQkxFRDogdHJ1ZQogICAgICBJTlRFUk5BTF9PVEVMX1RSQUNFX0RJU0FCTEVEOiAxCiAgICAgIElOVEVSTkFMX09URUxfVFJBQ0VfTE9HR0lOR19FTkFCTEVEOiAwCiAgICAgIFBPU1RHUkVTX1VTRVI6ICRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgUE9TVEdSRVNfUEFTU1dPUkQ6ICRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIFBPU1RHUkVTX0RCOiAnJHtQT1NUR1JFU19EQjotdHJpZ2dlcn0nCiAgICAgIE1BR0lDX0xJTktfU0VDUkVUOiAkU0VSVklDRV9QQVNTV09SRF82NF9NQUdJQwogICAgICBTRVNTSU9OX1NFQ1JFVDogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfU0VTU0lPTgogICAgICBFTkNSWVBUSU9OX0tFWTogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfRU5DUllQVElPTgogICAgICBQUk9WSURFUl9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X1BST1ZJREVSCiAgICAgIENPT1JESU5BVE9SX1NFQ1JFVDogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfQ09PUkRJTkFUT1IKICAgICAgREFUQUJBU0VfSE9TVDogJ3Bvc3RncmVzcWw6NTQzMicKICAgICAgREFUQUJBU0VfVVJMOiAncG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBvc3RncmVzcWw6NTQzMi8kUE9TVEdSRVNfREI/c3NsbW9kZT1kaXNhYmxlJwogICAgICBESVJFQ1RfVVJMOiAncG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBvc3RncmVzcWw6NTQzMi8kUE9TVEdSRVNfREI/c3NsbW9kZT1kaXNhYmxlJwogICAgICBSRURJU19IT1NUOiByZWRpcwogICAgICBSRURJU19QT1JUOiA2Mzc5CiAgICAgIFJFRElTX1RMU19ESVNBQkxFRDogdHJ1ZQogICAgICBDT09SRElOQVRPUl9IT1NUOiAxMjcuMC4wLjEKICAgICAgQ09PUkRJTkFUT1JfUE9SVDogOTAyMAogICAgICBXSElURUxJU1RFRF9FTUFJTFM6ICcnCiAgICAgIEFETUlOX0VNQUlMUzogJycKICAgICAgREVGQVVMVF9PUkdfRVhFQ1VUSU9OX0NPTkNVUlJFTkNZX0xJTUlUOiAzMDAKICAgICAgREVGQVVMVF9FTlZfRVhFQ1VUSU9OX0NPTkNVUlJFTkNZX0xJTUlUOiAxMDAKICAgICAgREVQTE9ZX1JFR0lTVFJZX0hPU1Q6IGRvY2tlci5pbwogICAgICBERVBMT1lfUkVHSVNUUllfTkFNRVNQQUNFOiB0cmlnZ2VyCiAgICAgIFJFR0lTVFJZX0hPU1Q6ICcke0RFUExPWV9SRUdJU1RSWV9IT1NUfScKICAgICAgUkVHSVNUUllfTkFNRVNQQUNFOiAnJHtERVBMT1lfUkVHSVNUUllfTkFNRVNQQUNFfScKICAgICAgQVVUSF9HSVRIVUJfQ0xJRU5UX0lEOiAnJHtBVVRIX0dJVEhVQl9DTElFTlRfSUR9JwogICAgICBBVVRIX0dJVEhVQl9DTElFTlRfU0VDUkVUOiAnJHtBVVRIX0dJVEhVQl9DTElFTlRfU0VDUkVUfScKICAgICAgUkVTRU5EX0FQSV9LRVk6ICcke1JFU0VORF9BUElfS0VZfScKICAgICAgRlJPTV9FTUFJTDogJyR7RlJPTV9FTUFJTH0nCiAgICAgIFJFUExZX1RPX0VNQUlMOiAnJHtSRVBMWV9UT19FTUFJTH0nCiAgICAgIExPR0lOX09SSUdJTjogJFNFUlZJQ0VfRlFETl9UUklHR0VSXzMwMDAKICAgICAgQVBQX09SSUdJTjogJFNFUlZJQ0VfRlFETl9UUklHR0VSXzMwMDAKICAgICAgREVWX09URUxfRVhQT1JURVJfT1RMUF9FTkRQT0lOVDogJFNFUlZJQ0VfRlFETl9UUklHR0VSXzMwMDAvb3RlbAogICAgICBPVEVMX0VYUE9SVEVSX09UTFBfRU5EUE9JTlQ6ICdodHRwOi8vdHJpZ2dlcjozMDQwL290ZWwnCiAgICAgIEVMRUNUUklDX09SSUdJTjogJ2h0dHA6Ly9lbGVjdHJpYzozMDAwJwogICAgICBQTEFURk9STV9IT1NUOiB0cmlnZ2VyCiAgICAgIFBMQVRGT1JNX1dTX1BPUlQ6IDMwMzAKICAgICAgU0VDVVJFX0NPTk5FQ1RJT046ICdmYWxzZScKICAgICAgUExBVEZPUk1fU0VDUkVUOiAkQ09PUkRJTkFUT1JfU0VDUkVUCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gcHdkCg==", + "compose": "eC1jb21tb24tZW52OgogIFJFTUlYX0FQUF9QT1JUOiAzMDAwCiAgTk9ERV9FTlY6IHByb2R1Y3Rpb24KICBSVU5USU1FX1BMQVRGT1JNOiBkb2NrZXItY29tcG9zZQogIFYzX0VOQUJMRUQ6IHRydWUKICBJTlRFUk5BTF9PVEVMX1RSQUNFX0RJU0FCTEVEOiAxCiAgSU5URVJOQUxfT1RFTF9UUkFDRV9MT0dHSU5HX0VOQUJMRUQ6IDAKICBQT1NUR1JFU19VU0VSOiAkU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgUE9TVEdSRVNfUEFTU1dPUkQ6ICRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgUE9TVEdSRVNfREI6ICcke1BPU1RHUkVTX0RCOi10cmlnZ2VyfScKICBNQUdJQ19MSU5LX1NFQ1JFVDogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfTUFHSUMKICBTRVNTSU9OX1NFQ1JFVDogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfU0VTU0lPTgogIEVOQ1JZUFRJT05fS0VZOiAkU0VSVklDRV9QQVNTV09SRF82NF9FTkNSWVBUSU9OCiAgUFJPVklERVJfU0VDUkVUOiAkU0VSVklDRV9QQVNTV09SRF82NF9QUk9WSURFUgogIENPT1JESU5BVE9SX1NFQ1JFVDogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfQ09PUkRJTkFUT1IKICBEQVRBQkFTRV9IT1NUOiAncG9zdGdyZXNxbDo1NDMyJwogIERBVEFCQVNFX1VSTDogJ3Bvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0Bwb3N0Z3Jlc3FsOjU0MzIvJFBPU1RHUkVTX0RCP3NzbG1vZGU9ZGlzYWJsZScKICBESVJFQ1RfVVJMOiAncG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBvc3RncmVzcWw6NTQzMi8kUE9TVEdSRVNfREI/c3NsbW9kZT1kaXNhYmxlJwogIFJFRElTX0hPU1Q6IHJlZGlzCiAgUkVESVNfUE9SVDogNjM3OQogIFJFRElTX1RMU19ESVNBQkxFRDogdHJ1ZQogIENPT1JESU5BVE9SX0hPU1Q6IDEyNy4wLjAuMQogIENPT1JESU5BVE9SX1BPUlQ6IDkwMjAKICBXSElURUxJU1RFRF9FTUFJTFM6ICcnCiAgQURNSU5fRU1BSUxTOiAnJwogIERFRkFVTFRfT1JHX0VYRUNVVElPTl9DT05DVVJSRU5DWV9MSU1JVDogMzAwCiAgREVGQVVMVF9FTlZfRVhFQ1VUSU9OX0NPTkNVUlJFTkNZX0xJTUlUOiAxMDAKICBERVBMT1lfUkVHSVNUUllfSE9TVDogZG9ja2VyLmlvCiAgREVQTE9ZX1JFR0lTVFJZX05BTUVTUEFDRTogdHJpZ2dlcgogIFJFR0lTVFJZX0hPU1Q6ICcke0RFUExPWV9SRUdJU1RSWV9IT1NUfScKICBSRUdJU1RSWV9OQU1FU1BBQ0U6ICcke0RFUExPWV9SRUdJU1RSWV9OQU1FU1BBQ0V9JwogIEFVVEhfR0lUSFVCX0NMSUVOVF9JRDogJyR7QVVUSF9HSVRIVUJfQ0xJRU5UX0lEfScKICBBVVRIX0dJVEhVQl9DTElFTlRfU0VDUkVUOiAnJHtBVVRIX0dJVEhVQl9DTElFTlRfU0VDUkVUfScKICBSRVNFTkRfQVBJX0tFWTogJyR7UkVTRU5EX0FQSV9LRVl9JwogIEZST01fRU1BSUw6ICcke0ZST01fRU1BSUx9JwogIFJFUExZX1RPX0VNQUlMOiAnJHtSRVBMWV9UT19FTUFJTH0nCiAgTE9HSU5fT1JJR0lOOiAkU0VSVklDRV9GUUROX1RSSUdHRVJfMzAwMAogIEFQUF9PUklHSU46ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwCiAgREVWX09URUxfRVhQT1JURVJfT1RMUF9FTkRQT0lOVDogJFNFUlZJQ0VfRlFETl9UUklHR0VSXzMwMDAvb3RlbAogIE9URUxfRVhQT1JURVJfT1RMUF9FTkRQT0lOVDogJ2h0dHA6Ly90cmlnZ2VyOjMwNDAvb3RlbCcKICBFTEVDVFJJQ19PUklHSU46ICdodHRwOi8vZWxlY3RyaWM6MzAwMCcKc2VydmljZXM6CiAgdHJpZ2dlcjoKICAgIGltYWdlOiAnZ2hjci5pby90cmlnZ2VyZG90ZGV2L3RyaWdnZXIuZGV2OnYzJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIFNFUlZJQ0VfRlFETl9UUklHR0VSXzMwMDA6ICcnCiAgICAgIFJFTUlYX0FQUF9QT1JUOiAzMDAwCiAgICAgIE5PREVfRU5WOiBwcm9kdWN0aW9uCiAgICAgIFJVTlRJTUVfUExBVEZPUk06IGRvY2tlci1jb21wb3NlCiAgICAgIFYzX0VOQUJMRUQ6IHRydWUKICAgICAgSU5URVJOQUxfT1RFTF9UUkFDRV9ESVNBQkxFRDogMQogICAgICBJTlRFUk5BTF9PVEVMX1RSQUNFX0xPR0dJTkdfRU5BQkxFRDogMAogICAgICBQT1NUR1JFU19VU0VSOiAkU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIFBPU1RHUkVTX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICBQT1NUR1JFU19EQjogJyR7UE9TVEdSRVNfREI6LXRyaWdnZXJ9JwogICAgICBNQUdJQ19MSU5LX1NFQ1JFVDogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfTUFHSUMKICAgICAgU0VTU0lPTl9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X1NFU1NJT04KICAgICAgRU5DUllQVElPTl9LRVk6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X0VOQ1JZUFRJT04KICAgICAgUFJPVklERVJfU0VDUkVUOiAkU0VSVklDRV9QQVNTV09SRF82NF9QUk9WSURFUgogICAgICBDT09SRElOQVRPUl9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X0NPT1JESU5BVE9SCiAgICAgIERBVEFCQVNFX0hPU1Q6ICdwb3N0Z3Jlc3FsOjU0MzInCiAgICAgIERBVEFCQVNFX1VSTDogJ3Bvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0Bwb3N0Z3Jlc3FsOjU0MzIvJFBPU1RHUkVTX0RCP3NzbG1vZGU9ZGlzYWJsZScKICAgICAgRElSRUNUX1VSTDogJ3Bvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0Bwb3N0Z3Jlc3FsOjU0MzIvJFBPU1RHUkVTX0RCP3NzbG1vZGU9ZGlzYWJsZScKICAgICAgUkVESVNfSE9TVDogcmVkaXMKICAgICAgUkVESVNfUE9SVDogNjM3OQogICAgICBSRURJU19UTFNfRElTQUJMRUQ6IHRydWUKICAgICAgQ09PUkRJTkFUT1JfSE9TVDogMTI3LjAuMC4xCiAgICAgIENPT1JESU5BVE9SX1BPUlQ6IDkwMjAKICAgICAgV0hJVEVMSVNURURfRU1BSUxTOiAnJwogICAgICBBRE1JTl9FTUFJTFM6ICcnCiAgICAgIERFRkFVTFRfT1JHX0VYRUNVVElPTl9DT05DVVJSRU5DWV9MSU1JVDogMzAwCiAgICAgIERFRkFVTFRfRU5WX0VYRUNVVElPTl9DT05DVVJSRU5DWV9MSU1JVDogMTAwCiAgICAgIERFUExPWV9SRUdJU1RSWV9IT1NUOiBkb2NrZXIuaW8KICAgICAgREVQTE9ZX1JFR0lTVFJZX05BTUVTUEFDRTogdHJpZ2dlcgogICAgICBSRUdJU1RSWV9IT1NUOiAnJHtERVBMT1lfUkVHSVNUUllfSE9TVH0nCiAgICAgIFJFR0lTVFJZX05BTUVTUEFDRTogJyR7REVQTE9ZX1JFR0lTVFJZX05BTUVTUEFDRX0nCiAgICAgIEFVVEhfR0lUSFVCX0NMSUVOVF9JRDogJyR7QVVUSF9HSVRIVUJfQ0xJRU5UX0lEfScKICAgICAgQVVUSF9HSVRIVUJfQ0xJRU5UX1NFQ1JFVDogJyR7QVVUSF9HSVRIVUJfQ0xJRU5UX1NFQ1JFVH0nCiAgICAgIFJFU0VORF9BUElfS0VZOiAnJHtSRVNFTkRfQVBJX0tFWX0nCiAgICAgIEZST01fRU1BSUw6ICcke0ZST01fRU1BSUx9JwogICAgICBSRVBMWV9UT19FTUFJTDogJyR7UkVQTFlfVE9fRU1BSUx9JwogICAgICBMT0dJTl9PUklHSU46ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwCiAgICAgIEFQUF9PUklHSU46ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwCiAgICAgIERFVl9PVEVMX0VYUE9SVEVSX09UTFBfRU5EUE9JTlQ6ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwL290ZWwKICAgICAgT1RFTF9FWFBPUlRFUl9PVExQX0VORFBPSU5UOiAnaHR0cDovL3RyaWdnZXI6MzA0MC9vdGVsJwogICAgICBFTEVDVFJJQ19PUklHSU46ICdodHRwOi8vZWxlY3RyaWM6MzAwMCcKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgZWxlY3RyaWM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OiAidGltZW91dCAxMHMgYmFzaCAtYyAnOj4gL2Rldi90Y3AvMTI3LjAuMC4xLzMwMDAnIHx8IGV4aXQgMSIKICAgICAgaW50ZXJ2YWw6IDEwcwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiA1CiAgZWxlY3RyaWM6CiAgICBpbWFnZTogZWxlY3RyaWNzcWwvZWxlY3RyaWMKICAgIGVudmlyb25tZW50OgogICAgICBSRU1JWF9BUFBfUE9SVDogMzAwMAogICAgICBOT0RFX0VOVjogcHJvZHVjdGlvbgogICAgICBSVU5USU1FX1BMQVRGT1JNOiBkb2NrZXItY29tcG9zZQogICAgICBWM19FTkFCTEVEOiB0cnVlCiAgICAgIElOVEVSTkFMX09URUxfVFJBQ0VfRElTQUJMRUQ6IDEKICAgICAgSU5URVJOQUxfT1RFTF9UUkFDRV9MT0dHSU5HX0VOQUJMRUQ6IDAKICAgICAgUE9TVEdSRVNfVVNFUjogJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICBQT1NUR1JFU19QQVNTV09SRDogJFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgUE9TVEdSRVNfREI6ICcke1BPU1RHUkVTX0RCOi10cmlnZ2VyfScKICAgICAgTUFHSUNfTElOS19TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X01BR0lDCiAgICAgIFNFU1NJT05fU0VDUkVUOiAkU0VSVklDRV9QQVNTV09SRF82NF9TRVNTSU9OCiAgICAgIEVOQ1JZUFRJT05fS0VZOiAkU0VSVklDRV9QQVNTV09SRF82NF9FTkNSWVBUSU9OCiAgICAgIFBST1ZJREVSX1NFQ1JFVDogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfUFJPVklERVIKICAgICAgQ09PUkRJTkFUT1JfU0VDUkVUOiAkU0VSVklDRV9QQVNTV09SRF82NF9DT09SRElOQVRPUgogICAgICBEQVRBQkFTRV9IT1NUOiAncG9zdGdyZXNxbDo1NDMyJwogICAgICBEQVRBQkFTRV9VUkw6ICdwb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcG9zdGdyZXNxbDo1NDMyLyRQT1NUR1JFU19EQj9zc2xtb2RlPWRpc2FibGUnCiAgICAgIERJUkVDVF9VUkw6ICdwb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcG9zdGdyZXNxbDo1NDMyLyRQT1NUR1JFU19EQj9zc2xtb2RlPWRpc2FibGUnCiAgICAgIFJFRElTX0hPU1Q6IHJlZGlzCiAgICAgIFJFRElTX1BPUlQ6IDYzNzkKICAgICAgUkVESVNfVExTX0RJU0FCTEVEOiB0cnVlCiAgICAgIENPT1JESU5BVE9SX0hPU1Q6IDEyNy4wLjAuMQogICAgICBDT09SRElOQVRPUl9QT1JUOiA5MDIwCiAgICAgIFdISVRFTElTVEVEX0VNQUlMUzogJycKICAgICAgQURNSU5fRU1BSUxTOiAnJwogICAgICBERUZBVUxUX09SR19FWEVDVVRJT05fQ09OQ1VSUkVOQ1lfTElNSVQ6IDMwMAogICAgICBERUZBVUxUX0VOVl9FWEVDVVRJT05fQ09OQ1VSUkVOQ1lfTElNSVQ6IDEwMAogICAgICBERVBMT1lfUkVHSVNUUllfSE9TVDogZG9ja2VyLmlvCiAgICAgIERFUExPWV9SRUdJU1RSWV9OQU1FU1BBQ0U6IHRyaWdnZXIKICAgICAgUkVHSVNUUllfSE9TVDogJyR7REVQTE9ZX1JFR0lTVFJZX0hPU1R9JwogICAgICBSRUdJU1RSWV9OQU1FU1BBQ0U6ICcke0RFUExPWV9SRUdJU1RSWV9OQU1FU1BBQ0V9JwogICAgICBBVVRIX0dJVEhVQl9DTElFTlRfSUQ6ICcke0FVVEhfR0lUSFVCX0NMSUVOVF9JRH0nCiAgICAgIEFVVEhfR0lUSFVCX0NMSUVOVF9TRUNSRVQ6ICcke0FVVEhfR0lUSFVCX0NMSUVOVF9TRUNSRVR9JwogICAgICBSRVNFTkRfQVBJX0tFWTogJyR7UkVTRU5EX0FQSV9LRVl9JwogICAgICBGUk9NX0VNQUlMOiAnJHtGUk9NX0VNQUlMfScKICAgICAgUkVQTFlfVE9fRU1BSUw6ICcke1JFUExZX1RPX0VNQUlMfScKICAgICAgTE9HSU5fT1JJR0lOOiAkU0VSVklDRV9GUUROX1RSSUdHRVJfMzAwMAogICAgICBBUFBfT1JJR0lOOiAkU0VSVklDRV9GUUROX1RSSUdHRVJfMzAwMAogICAgICBERVZfT1RFTF9FWFBPUlRFUl9PVExQX0VORFBPSU5UOiAkU0VSVklDRV9GUUROX1RSSUdHRVJfMzAwMC9vdGVsCiAgICAgIE9URUxfRVhQT1JURVJfT1RMUF9FTkRQT0lOVDogJ2h0dHA6Ly90cmlnZ2VyOjMwNDAvb3RlbCcKICAgICAgRUxFQ1RSSUNfT1JJR0lOOiAnaHR0cDovL2VsZWN0cmljOjMwMDAnCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3Jlc3FsOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gcHdkCiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOjcnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBBTExPV19FTVBUWV9QQVNTV09SRD15ZXMKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncmVkaXMtY2xpIC1oIGxvY2FsaG9zdCAtcCA2Mzc5IHBpbmcnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiAzCiAgICB2b2x1bWVzOgogICAgICAtICdyZWRpcy1kYXRhOi9kYXRhJwogIHBvc3RncmVzcWw6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgUkVNSVhfQVBQX1BPUlQ6IDMwMDAKICAgICAgTk9ERV9FTlY6IHByb2R1Y3Rpb24KICAgICAgUlVOVElNRV9QTEFURk9STTogZG9ja2VyLWNvbXBvc2UKICAgICAgVjNfRU5BQkxFRDogdHJ1ZQogICAgICBJTlRFUk5BTF9PVEVMX1RSQUNFX0RJU0FCTEVEOiAxCiAgICAgIElOVEVSTkFMX09URUxfVFJBQ0VfTE9HR0lOR19FTkFCTEVEOiAwCiAgICAgIFBPU1RHUkVTX1VTRVI6ICRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgUE9TVEdSRVNfUEFTU1dPUkQ6ICRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIFBPU1RHUkVTX0RCOiAnJHtQT1NUR1JFU19EQjotdHJpZ2dlcn0nCiAgICAgIE1BR0lDX0xJTktfU0VDUkVUOiAkU0VSVklDRV9QQVNTV09SRF82NF9NQUdJQwogICAgICBTRVNTSU9OX1NFQ1JFVDogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfU0VTU0lPTgogICAgICBFTkNSWVBUSU9OX0tFWTogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfRU5DUllQVElPTgogICAgICBQUk9WSURFUl9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X1BST1ZJREVSCiAgICAgIENPT1JESU5BVE9SX1NFQ1JFVDogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfQ09PUkRJTkFUT1IKICAgICAgREFUQUJBU0VfSE9TVDogJ3Bvc3RncmVzcWw6NTQzMicKICAgICAgREFUQUJBU0VfVVJMOiAncG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBvc3RncmVzcWw6NTQzMi8kUE9TVEdSRVNfREI/c3NsbW9kZT1kaXNhYmxlJwogICAgICBESVJFQ1RfVVJMOiAncG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBvc3RncmVzcWw6NTQzMi8kUE9TVEdSRVNfREI/c3NsbW9kZT1kaXNhYmxlJwogICAgICBSRURJU19IT1NUOiByZWRpcwogICAgICBSRURJU19QT1JUOiA2Mzc5CiAgICAgIFJFRElTX1RMU19ESVNBQkxFRDogdHJ1ZQogICAgICBDT09SRElOQVRPUl9IT1NUOiAxMjcuMC4wLjEKICAgICAgQ09PUkRJTkFUT1JfUE9SVDogOTAyMAogICAgICBXSElURUxJU1RFRF9FTUFJTFM6ICcnCiAgICAgIEFETUlOX0VNQUlMUzogJycKICAgICAgREVGQVVMVF9PUkdfRVhFQ1VUSU9OX0NPTkNVUlJFTkNZX0xJTUlUOiAzMDAKICAgICAgREVGQVVMVF9FTlZfRVhFQ1VUSU9OX0NPTkNVUlJFTkNZX0xJTUlUOiAxMDAKICAgICAgREVQTE9ZX1JFR0lTVFJZX0hPU1Q6IGRvY2tlci5pbwogICAgICBERVBMT1lfUkVHSVNUUllfTkFNRVNQQUNFOiB0cmlnZ2VyCiAgICAgIFJFR0lTVFJZX0hPU1Q6ICcke0RFUExPWV9SRUdJU1RSWV9IT1NUfScKICAgICAgUkVHSVNUUllfTkFNRVNQQUNFOiAnJHtERVBMT1lfUkVHSVNUUllfTkFNRVNQQUNFfScKICAgICAgQVVUSF9HSVRIVUJfQ0xJRU5UX0lEOiAnJHtBVVRIX0dJVEhVQl9DTElFTlRfSUR9JwogICAgICBBVVRIX0dJVEhVQl9DTElFTlRfU0VDUkVUOiAnJHtBVVRIX0dJVEhVQl9DTElFTlRfU0VDUkVUfScKICAgICAgUkVTRU5EX0FQSV9LRVk6ICcke1JFU0VORF9BUElfS0VZfScKICAgICAgRlJPTV9FTUFJTDogJyR7RlJPTV9FTUFJTH0nCiAgICAgIFJFUExZX1RPX0VNQUlMOiAnJHtSRVBMWV9UT19FTUFJTH0nCiAgICAgIExPR0lOX09SSUdJTjogJFNFUlZJQ0VfRlFETl9UUklHR0VSXzMwMDAKICAgICAgQVBQX09SSUdJTjogJFNFUlZJQ0VfRlFETl9UUklHR0VSXzMwMDAKICAgICAgREVWX09URUxfRVhQT1JURVJfT1RMUF9FTkRQT0lOVDogJFNFUlZJQ0VfRlFETl9UUklHR0VSXzMwMDAvb3RlbAogICAgICBPVEVMX0VYUE9SVEVSX09UTFBfRU5EUE9JTlQ6ICdodHRwOi8vdHJpZ2dlcjozMDQwL290ZWwnCiAgICAgIEVMRUNUUklDX09SSUdJTjogJ2h0dHA6Ly9lbGVjdHJpYzozMDAwJwogICAgY29tbWFuZDoKICAgICAgLSAnLWMnCiAgICAgIC0gd2FsX2xldmVsPWxvZ2ljYWwKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBkb2NrZXItcHJvdmlkZXI6CiAgICBpbWFnZTogJ2doY3IuaW8vdHJpZ2dlcmRvdGRldi9wcm92aWRlci9kb2NrZXI6djMnCiAgICBwbGF0Zm9ybTogbGludXgvYW1kNjQKICAgIHZvbHVtZXM6CiAgICAgIC0gJy92YXIvcnVuL2RvY2tlci5zb2NrOi92YXIvcnVuL2RvY2tlci5zb2NrJwogICAgdXNlcjogcm9vdAogICAgZGVwZW5kc19vbjoKICAgICAgdHJpZ2dlcjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIFJFTUlYX0FQUF9QT1JUOiAzMDAwCiAgICAgIE5PREVfRU5WOiBwcm9kdWN0aW9uCiAgICAgIFJVTlRJTUVfUExBVEZPUk06IGRvY2tlci1jb21wb3NlCiAgICAgIFYzX0VOQUJMRUQ6IHRydWUKICAgICAgSU5URVJOQUxfT1RFTF9UUkFDRV9ESVNBQkxFRDogMQogICAgICBJTlRFUk5BTF9PVEVMX1RSQUNFX0xPR0dJTkdfRU5BQkxFRDogMAogICAgICBQT1NUR1JFU19VU0VSOiAkU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIFBPU1RHUkVTX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICBQT1NUR1JFU19EQjogJyR7UE9TVEdSRVNfREI6LXRyaWdnZXJ9JwogICAgICBNQUdJQ19MSU5LX1NFQ1JFVDogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfTUFHSUMKICAgICAgU0VTU0lPTl9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X1NFU1NJT04KICAgICAgRU5DUllQVElPTl9LRVk6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X0VOQ1JZUFRJT04KICAgICAgUFJPVklERVJfU0VDUkVUOiAkU0VSVklDRV9QQVNTV09SRF82NF9QUk9WSURFUgogICAgICBDT09SRElOQVRPUl9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X0NPT1JESU5BVE9SCiAgICAgIERBVEFCQVNFX0hPU1Q6ICdwb3N0Z3Jlc3FsOjU0MzInCiAgICAgIERBVEFCQVNFX1VSTDogJ3Bvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0Bwb3N0Z3Jlc3FsOjU0MzIvJFBPU1RHUkVTX0RCP3NzbG1vZGU9ZGlzYWJsZScKICAgICAgRElSRUNUX1VSTDogJ3Bvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0Bwb3N0Z3Jlc3FsOjU0MzIvJFBPU1RHUkVTX0RCP3NzbG1vZGU9ZGlzYWJsZScKICAgICAgUkVESVNfSE9TVDogcmVkaXMKICAgICAgUkVESVNfUE9SVDogNjM3OQogICAgICBSRURJU19UTFNfRElTQUJMRUQ6IHRydWUKICAgICAgQ09PUkRJTkFUT1JfSE9TVDogMTI3LjAuMC4xCiAgICAgIENPT1JESU5BVE9SX1BPUlQ6IDkwMjAKICAgICAgV0hJVEVMSVNURURfRU1BSUxTOiAnJwogICAgICBBRE1JTl9FTUFJTFM6ICcnCiAgICAgIERFRkFVTFRfT1JHX0VYRUNVVElPTl9DT05DVVJSRU5DWV9MSU1JVDogMzAwCiAgICAgIERFRkFVTFRfRU5WX0VYRUNVVElPTl9DT05DVVJSRU5DWV9MSU1JVDogMTAwCiAgICAgIERFUExPWV9SRUdJU1RSWV9IT1NUOiBkb2NrZXIuaW8KICAgICAgREVQTE9ZX1JFR0lTVFJZX05BTUVTUEFDRTogdHJpZ2dlcgogICAgICBSRUdJU1RSWV9IT1NUOiAnJHtERVBMT1lfUkVHSVNUUllfSE9TVH0nCiAgICAgIFJFR0lTVFJZX05BTUVTUEFDRTogJyR7REVQTE9ZX1JFR0lTVFJZX05BTUVTUEFDRX0nCiAgICAgIEFVVEhfR0lUSFVCX0NMSUVOVF9JRDogJyR7QVVUSF9HSVRIVUJfQ0xJRU5UX0lEfScKICAgICAgQVVUSF9HSVRIVUJfQ0xJRU5UX1NFQ1JFVDogJyR7QVVUSF9HSVRIVUJfQ0xJRU5UX1NFQ1JFVH0nCiAgICAgIFJFU0VORF9BUElfS0VZOiAnJHtSRVNFTkRfQVBJX0tFWX0nCiAgICAgIEZST01fRU1BSUw6ICcke0ZST01fRU1BSUx9JwogICAgICBSRVBMWV9UT19FTUFJTDogJyR7UkVQTFlfVE9fRU1BSUx9JwogICAgICBMT0dJTl9PUklHSU46ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwCiAgICAgIEFQUF9PUklHSU46ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwCiAgICAgIERFVl9PVEVMX0VYUE9SVEVSX09UTFBfRU5EUE9JTlQ6ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwL290ZWwKICAgICAgT1RFTF9FWFBPUlRFUl9PVExQX0VORFBPSU5UOiAnaHR0cDovL3RyaWdnZXI6MzA0MC9vdGVsJwogICAgICBFTEVDVFJJQ19PUklHSU46ICdodHRwOi8vZWxlY3RyaWM6MzAwMCcKICAgICAgUExBVEZPUk1fSE9TVDogdHJpZ2dlcgogICAgICBQTEFURk9STV9XU19QT1JUOiAzMDAwCiAgICAgIFNFQ1VSRV9DT05ORUNUSU9OOiAnZmFsc2UnCiAgICAgIFBMQVRGT1JNX1NFQ1JFVDogJFBST1ZJREVSX1NFQ1JFVAogIGNvb3JkaW5hdG9yOgogICAgaW1hZ2U6ICdnaGNyLmlvL3RyaWdnZXJkb3RkZXYvY29vcmRpbmF0b3I6djMnCiAgICBwbGF0Zm9ybTogbGludXgvYW1kNjQKICAgIHZvbHVtZXM6CiAgICAgIC0gJy92YXIvcnVuL2RvY2tlci5zb2NrOi92YXIvcnVuL2RvY2tlci5zb2NrJwogICAgdXNlcjogcm9vdAogICAgZGVwZW5kc19vbjoKICAgICAgdHJpZ2dlcjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIFJFTUlYX0FQUF9QT1JUOiAzMDAwCiAgICAgIE5PREVfRU5WOiBwcm9kdWN0aW9uCiAgICAgIFJVTlRJTUVfUExBVEZPUk06IGRvY2tlci1jb21wb3NlCiAgICAgIFYzX0VOQUJMRUQ6IHRydWUKICAgICAgSU5URVJOQUxfT1RFTF9UUkFDRV9ESVNBQkxFRDogMQogICAgICBJTlRFUk5BTF9PVEVMX1RSQUNFX0xPR0dJTkdfRU5BQkxFRDogMAogICAgICBQT1NUR1JFU19VU0VSOiAkU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIFBPU1RHUkVTX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICBQT1NUR1JFU19EQjogJyR7UE9TVEdSRVNfREI6LXRyaWdnZXJ9JwogICAgICBNQUdJQ19MSU5LX1NFQ1JFVDogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfTUFHSUMKICAgICAgU0VTU0lPTl9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X1NFU1NJT04KICAgICAgRU5DUllQVElPTl9LRVk6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X0VOQ1JZUFRJT04KICAgICAgUFJPVklERVJfU0VDUkVUOiAkU0VSVklDRV9QQVNTV09SRF82NF9QUk9WSURFUgogICAgICBDT09SRElOQVRPUl9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X0NPT1JESU5BVE9SCiAgICAgIERBVEFCQVNFX0hPU1Q6ICdwb3N0Z3Jlc3FsOjU0MzInCiAgICAgIERBVEFCQVNFX1VSTDogJ3Bvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0Bwb3N0Z3Jlc3FsOjU0MzIvJFBPU1RHUkVTX0RCP3NzbG1vZGU9ZGlzYWJsZScKICAgICAgRElSRUNUX1VSTDogJ3Bvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0Bwb3N0Z3Jlc3FsOjU0MzIvJFBPU1RHUkVTX0RCP3NzbG1vZGU9ZGlzYWJsZScKICAgICAgUkVESVNfSE9TVDogcmVkaXMKICAgICAgUkVESVNfUE9SVDogNjM3OQogICAgICBSRURJU19UTFNfRElTQUJMRUQ6IHRydWUKICAgICAgQ09PUkRJTkFUT1JfSE9TVDogMTI3LjAuMC4xCiAgICAgIENPT1JESU5BVE9SX1BPUlQ6IDkwMjAKICAgICAgV0hJVEVMSVNURURfRU1BSUxTOiAnJwogICAgICBBRE1JTl9FTUFJTFM6ICcnCiAgICAgIERFRkFVTFRfT1JHX0VYRUNVVElPTl9DT05DVVJSRU5DWV9MSU1JVDogMzAwCiAgICAgIERFRkFVTFRfRU5WX0VYRUNVVElPTl9DT05DVVJSRU5DWV9MSU1JVDogMTAwCiAgICAgIERFUExPWV9SRUdJU1RSWV9IT1NUOiBkb2NrZXIuaW8KICAgICAgREVQTE9ZX1JFR0lTVFJZX05BTUVTUEFDRTogdHJpZ2dlcgogICAgICBSRUdJU1RSWV9IT1NUOiAnJHtERVBMT1lfUkVHSVNUUllfSE9TVH0nCiAgICAgIFJFR0lTVFJZX05BTUVTUEFDRTogJyR7REVQTE9ZX1JFR0lTVFJZX05BTUVTUEFDRX0nCiAgICAgIEFVVEhfR0lUSFVCX0NMSUVOVF9JRDogJyR7QVVUSF9HSVRIVUJfQ0xJRU5UX0lEfScKICAgICAgQVVUSF9HSVRIVUJfQ0xJRU5UX1NFQ1JFVDogJyR7QVVUSF9HSVRIVUJfQ0xJRU5UX1NFQ1JFVH0nCiAgICAgIFJFU0VORF9BUElfS0VZOiAnJHtSRVNFTkRfQVBJX0tFWX0nCiAgICAgIEZST01fRU1BSUw6ICcke0ZST01fRU1BSUx9JwogICAgICBSRVBMWV9UT19FTUFJTDogJyR7UkVQTFlfVE9fRU1BSUx9JwogICAgICBMT0dJTl9PUklHSU46ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwCiAgICAgIEFQUF9PUklHSU46ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwCiAgICAgIERFVl9PVEVMX0VYUE9SVEVSX09UTFBfRU5EUE9JTlQ6ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwL290ZWwKICAgICAgT1RFTF9FWFBPUlRFUl9PVExQX0VORFBPSU5UOiAnaHR0cDovL3RyaWdnZXI6MzA0MC9vdGVsJwogICAgICBFTEVDVFJJQ19PUklHSU46ICdodHRwOi8vZWxlY3RyaWM6MzAwMCcKICAgICAgUExBVEZPUk1fSE9TVDogdHJpZ2dlcgogICAgICBQTEFURk9STV9XU19QT1JUOiAzMDAwCiAgICAgIFNFQ1VSRV9DT05ORUNUSU9OOiAnZmFsc2UnCiAgICAgIFBMQVRGT1JNX1NFQ1JFVDogJENPT1JESU5BVE9SX1NFQ1JFVAogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtIHB3ZAo=", "tags": [ "trigger.dev", "background jobs", @@ -2971,7 +2971,7 @@ "privacy", "search engine" ], - "logo": "svgs/default.webp", + "logo": "svgs/whoogle.png", "minversion": "0.0.0", "port": "5000" },