Compare commits

...

63 Commits

Author SHA1 Message Date
Andras Bacsai
a7df9fa625 Merge pull request #1798 from coollabsio/next
v4.0.0-beta.231
2024-03-02 15:04:48 +01:00
Andras Bacsai
9064aedc89 Fix server reference in ExecuteContainerCommand.php 2024-03-02 15:02:55 +01:00
Andras Bacsai
fda5d23d32 feat: logs and execute commands with several servers 2024-03-02 14:55:39 +01:00
Andras Bacsai
60be51dbe0 Update pull_request_id comparison in ApplicationDeploymentJob.php and update version numbers 2024-03-02 13:22:05 +01:00
Andras Bacsai
e9f451339f Merge pull request #1796 from coollabsio/next
fix: unmanaged containers method
2024-03-01 19:14:18 +01:00
Andras Bacsai
4d8ffd05a9 Refactor unmanagedContainers property in Resources.php and add conditional return in loadUnmanagedContainers() method 2024-03-01 19:13:22 +01:00
Andras Bacsai
b630105572 Merge pull request #1795 from coollabsio/next
v4.0.0-beta.230
2024-03-01 19:09:09 +01:00
Andras Bacsai
9fa71f847f Refactor notification channels based on cloud environment 2024-03-01 19:08:00 +01:00
Andras Bacsai
f70a9c6974 Fix notification channels in ApplicationDeploymentJob and DeploymentSuccess 2024-03-01 19:07:21 +01:00
Andras Bacsai
a4d173c733 Fix unmanagedContainers type declaration 2024-03-01 19:00:45 +01:00
Andras Bacsai
2eb7712e09 fix: remove success application deployment job
wip: daily backup status
2024-03-01 18:24:14 +01:00
Andras Bacsai
54923b7640 feat: collect webhooks during maintenance 2024-03-01 14:04:29 +01:00
Andras Bacsai
bb927505fe Merge pull request #1793 from coollabsio/next
v4.0.0-beta.229
2024-03-01 11:47:14 +01:00
Andras Bacsai
5e66e314d2 Update version numbers to 4.0.0-beta.229 2024-03-01 11:44:01 +01:00
Andras Bacsai
6fe791c1f1 fix: pull request deployments + build servers 2024-03-01 11:43:42 +01:00
Andras Bacsai
860c537f81 Add server limit override for development environment 2024-03-01 11:41:28 +01:00
Andras Bacsai
a352e4cbf7 fix: public prs should not be commented 2024-03-01 11:41:22 +01:00
Andras Bacsai
5322d446bd fix: service container status updates 2024-03-01 10:36:32 +01:00
Andras Bacsai
604ab0afd8 Add autofocus to search input field 2024-03-01 10:06:59 +01:00
Andras Bacsai
3d87a88d3d Merge pull request #1790 from coollabsio/next
v4.0.0-beta.228
2024-03-01 09:32:19 +01:00
Andras Bacsai
10f9e22a8e fix: do not show n/a networsk 2024-03-01 09:28:14 +01:00
Andras Bacsai
8edda0cdda fix: load unmanaged async 2024-03-01 09:25:27 +01:00
Andras Bacsai
21047afc02 Add new sponsor image 2024-03-01 09:19:23 +01:00
Andras Bacsai
2e9793ffb2 Refactor code for improved performance and readability 2024-02-29 09:21:02 +01:00
Andras Bacsai
fcd100df39 Fix typos and grammatical errors in email templates and form view 2024-02-29 09:16:02 +01:00
Andras Bacsai
dfd564a3a4 Add Supabase logo and update environment variable in compose file 2024-02-29 09:15:06 +01:00
Andras Bacsai
a43c916009 Refactor code and add new fields for Kong service 2024-02-28 13:48:39 +01:00
Andras Bacsai
c8332ca9bf fix: resource tab not loading if server is not reachable 2024-02-28 09:51:45 +01:00
Andras Bacsai
e98170f921 Update Github Sponsors to $40+ 2024-02-28 09:38:59 +01:00
Andras Bacsai
b8f25406cd Refactor code to improve performance and readability 2024-02-27 15:44:19 +01:00
Andras Bacsai
76dcc12b13 Update version numbers to 4.0.0-beta.228 2024-02-27 15:13:30 +01:00
Andras Bacsai
baa2228c9b Merge pull request #1786 from coollabsio/next
v4.0.0-beta.226
2024-02-27 09:10:31 +01:00
Andras Bacsai
5275ae8e9c Refactor getLogs method and update view template 2024-02-27 09:08:15 +01:00
Andras Bacsai
c71e1e107e Refactor getLogs method and update get-logs.blade.php view 2024-02-27 09:05:28 +01:00
Andras Bacsai
8ab72c7e10 feat: preview deployment logs 2024-02-27 09:01:19 +01:00
Andras Bacsai
a8970df91b Update class names in controllers 2024-02-27 08:03:42 +01:00
Andras Bacsai
2468251f56 Merge pull request #1783 from coollabsio/next
v4.0.0-beta.226
2024-02-26 14:31:16 +01:00
Andras Bacsai
6e74f3e40e Merge pull request #1779 from Rei-x/next
Fix import to mysql and mariadb
2024-02-26 14:30:35 +01:00
Andras Bacsai
407f84a4bb Refactor Dockerfile location handling in ApplicationDeploymentJob.php 2024-02-26 14:28:02 +01:00
Andras Bacsai
91632f0adb fix: custom dockerfile location always checked 2024-02-26 14:26:19 +01:00
Andras Bacsai
af3c575d84 fix: server disabled 2024-02-26 14:22:24 +01:00
Andras Bacsai
bf1475441d Update service stop message and fix sidebar alignment 2024-02-26 12:38:15 +01:00
Andras Bacsai
9268f9db1d Refactor user switching logic and update UI 2024-02-26 11:48:35 +01:00
Andras Bacsai
600c43827a Update server check and version numbers 2024-02-26 11:25:38 +01:00
Andras Bacsai
74092ea95b Merge pull request #1776 from coollabsio/next
4.0.0-beta.225
2024-02-26 11:08:53 +01:00
Andras Bacsai
b67abe58e8 Remove commented out code in ServerStatusJob.php 2024-02-26 10:34:44 +01:00
Andras Bacsai
678647f39a fix: force enable/disable server in case ultimate package quantity decreases 2024-02-26 10:25:21 +01:00
Andras Bacsai
453956172b Refactor show.blade.php to improve code readability 2024-02-26 09:32:28 +01:00
Andras Bacsai
b550c32f9b Add whitespace-pre-line class to font-mono in deployment show blade file 2024-02-26 09:09:01 +01:00
Andras Bacsai
f6b886adbc revert delayed jobs for now 2024-02-26 08:52:46 +01:00
Andras Bacsai
9642453052 fix: firefly service 2024-02-26 08:52:17 +01:00
Andras Bacsai
64fca99c26 feat: server disabled by overflow 2024-02-25 23:34:01 +01:00
Andras Bacsai
c7da43f50d feat: add static ipv4 ipv6 support 2024-02-25 23:13:27 +01:00
Rei
6efa2dd9ba fix: import to mysql and mariadb 2024-02-25 22:15:48 +01:00
Andras Bacsai
5e980c5fe0 Update pricing plans layout and text 2024-02-25 22:14:20 +01:00
Andras Bacsai
c8c7a415ea Add new Livewire component and update subscription actions 2024-02-25 22:08:44 +01:00
Andras Bacsai
c3cfb8d23b Refactor getRecepients method and fix serverLimitReached method in Team model 2024-02-25 18:22:24 +01:00
Andras Bacsai
1b055f0316 Refactor subscription pricing and update server limit 2024-02-25 14:00:35 +01:00
Andras Bacsai
1fcbf0b363 Update pricing plans display and button text 2024-02-23 22:14:24 +01:00
Andras Bacsai
61dbc81765 feat: delay container/server jobs 2024-02-23 21:51:43 +01:00
Andras Bacsai
b8b76dfa40 Refactor CleanupQueue to CleanupDatabase 2024-02-23 21:05:48 +01:00
Andras Bacsai
297b314904 feat: custom server limit 2024-02-23 15:45:53 +01:00
Andras Bacsai
55dd1ab0a1 Update cleanup script and version numbers 2024-02-23 14:39:52 +01:00
104 changed files with 4033 additions and 1596 deletions

View File

@@ -33,7 +33,9 @@ Special thanks to our biggest sponsors, [CCCareers](https://cccareers.org/) and
<a href="https://cccareers.org/" target="_blank"><img src="./other/logos/ccc-logo.webp" alt="cccareers logo" width="200"/></a> <a href="https://cccareers.org/" target="_blank"><img src="./other/logos/ccc-logo.webp" alt="cccareers logo" width="200"/></a>
<a href="https://appwrite.io" target="_blank"><img src="./other/logos/appwrite.svg" alt="appwrite logo" width="200"/></a> <a href="https://appwrite.io" target="_blank"><img src="./other/logos/appwrite.svg" alt="appwrite logo" width="200"/></a>
## Github Sponsors ($15+) ## Github Sponsors ($40+)
<a href="https://cryptojobslist.com/?utm_source=coolify.io"><img src="https://github.com/cryptojobslist.png" width="60px" alt="CryptoJobsList" /></a>
<a href="https://typebot.io/?utm_source=coolify.io"><img src="https://pbs.twimg.com/profile_images/1509194008366657543/9I-C7uWT_400x400.jpg" width="60px" alt="typebot"/></a>
<a href="https://bc.direct"><img width="60px" alt="BC Direct" src="https://github.com/coollabsio/coolify/assets/5845193/a4063c41-95ed-4a32-8814-cd1475572e37"/></a> <a href="https://bc.direct"><img width="60px" alt="BC Direct" src="https://github.com/coollabsio/coolify/assets/5845193/a4063c41-95ed-4a32-8814-cd1475572e37"/></a>
<a href="https://github.com/automazeio"><img src="https://github.com/automazeio.png" width="60px" alt="Corentin Clichy" /></a> <a href="https://github.com/automazeio"><img src="https://github.com/automazeio.png" width="60px" alt="Corentin Clichy" /></a>
<a href="https://github.com/corentinclichy"><img src="https://github.com/corentinclichy.png" width="60px" alt="Corentin Clichy" /></a> <a href="https://github.com/corentinclichy"><img src="https://github.com/corentinclichy.png" width="60px" alt="Corentin Clichy" /></a>

View File

@@ -16,7 +16,7 @@ class StartService
$commands[] = "cd " . $service->workdir(); $commands[] = "cd " . $service->workdir();
$commands[] = "echo 'Saved configuration files to {$service->workdir()}.'"; $commands[] = "echo 'Saved configuration files to {$service->workdir()}.'";
$commands[] = "echo 'Creating Docker network.'"; $commands[] = "echo 'Creating Docker network.'";
$commands[] = "docker network inspect $service->uuid >/dev/null 2>&1 || docker network create --attachable $service->uuid >/dev/null 2>&1 || true"; $commands[] = "docker network inspect $service->uuid >/dev/null 2>&1 || docker network create --attachable $service->uuid";
$commands[] = "echo Starting service."; $commands[] = "echo Starting service.";
$commands[] = "echo 'Pulling images.'"; $commands[] = "echo 'Pulling images.'";
$commands[] = "docker compose pull"; $commands[] = "docker compose pull";

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Console\Commands;
use App\Models\ApplicationDeploymentQueue;
use Illuminate\Console\Command;
class CleanupApplicationDeploymentQueue extends Command
{
protected $signature = 'cleanup:application-deployment-queue {--team-id=}';
protected $description = 'CleanupApplicationDeploymentQueue';
public function handle()
{
$team_id = $this->option('team-id');
$servers = \App\Models\Server::where('team_id', $team_id)->get();
foreach ($servers as $server) {
$deployments = ApplicationDeploymentQueue::whereIn("status", ["in_progress", "queued"])->where("server_id", $server->id)->get();
foreach ($deployments as $deployment) {
$deployment->update(['status' => 'failed']);
instant_remote_process(['docker rm -f ' . $deployment->deployment_uuid], $server, false);
}
}
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class CleanupDatabase extends Command
{
protected $signature = 'cleanup:database {--yes}';
protected $description = 'Cleanup database';
public function handle()
{
echo "Running database cleanup...\n";
$keep_days = 60;
// Cleanup failed jobs table
$failed_jobs = DB::table('failed_jobs')->where('failed_at', '<', now()->subDays(7));
$count = $failed_jobs->count();
echo "Delete $count entries from failed_jobs.\n";
if ($this->option('yes')) {
$failed_jobs->delete();
}
// Cleanup sessions table
$sessions = DB::table('sessions')->where('last_activity', '<', now()->subDays($keep_days)->timestamp);
$count = $sessions->count();
echo "Delete $count entries from sessions.\n";
if ($this->option('yes')) {
$sessions->delete();
}
// Cleanup activity_log table
$activity_log = DB::table('activity_log')->where('created_at', '<', now()->subDays($keep_days));
$count = $activity_log->count();
echo "Delete $count entries from activity_log.\n";
if ($this->option('yes')) {
$activity_log->delete();
}
// Cleanup application_deployment_queues table
$application_deployment_queues = DB::table('application_deployment_queues')->where('created_at', '<', now()->subDays($keep_days));
$count = $application_deployment_queues->count();
echo "Delete $count entries from application_deployment_queues.\n";
if ($this->option('yes')) {
$application_deployment_queues->delete();
}
// Cleanup webhooks table
$webhooks = DB::table('webhooks')->where('created_at', '<', now()->subDays($keep_days));
$count = $webhooks->count();
echo "Delete $count entries from webhooks.\n";
if ($this->option('yes')) {
$webhooks->delete();
}
}
}

View File

@@ -8,15 +8,16 @@ use Illuminate\Console\Command;
class CleanupUnreachableServers extends Command class CleanupUnreachableServers extends Command
{ {
protected $signature = 'cleanup:unreachable-servers'; protected $signature = 'cleanup:unreachable-servers';
protected $description = 'Cleanup Unreachable Servers (3 days)'; protected $description = 'Cleanup Unreachable Servers (7 days)';
public function handle() public function handle()
{ {
echo "Running unreachable server cleanup...\n"; echo "Running unreachable server cleanup...\n";
$servers = Server::where('unreachable_count', 3)->where('unreachable_notification_sent', true)->where('updated_at', '<', now()->subDays(3))->get(); $servers = Server::where('unreachable_count', 3)->where('unreachable_notification_sent', true)->where('updated_at', '<', now()->subDays(7))->get();
if ($servers->count() > 0) { if ($servers->count() > 0) {
foreach ($servers as $server) { foreach ($servers as $server) {
echo "Cleanup unreachable server ($server->id) with name $server->name"; echo "Cleanup unreachable server ($server->id) with name $server->name";
send_internal_notification("Server $server->name is unreachable for 7 days. Cleaning up...");
$server->update([ $server->update([
'ip' => '1.2.3.4' 'ip' => '1.2.3.4'
]); ]);

View File

@@ -2,10 +2,12 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Jobs\DatabaseBackupStatusJob;
use App\Jobs\SendConfirmationForWaitlistJob; use App\Jobs\SendConfirmationForWaitlistJob;
use App\Models\Application; use App\Models\Application;
use App\Models\ApplicationPreview; use App\Models\ApplicationPreview;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledDatabaseBackupExecution;
use App\Models\Server; use App\Models\Server;
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
use App\Models\Team; use App\Models\Team;
@@ -15,6 +17,7 @@ use App\Notifications\Application\DeploymentSuccess;
use App\Notifications\Application\StatusChanged; use App\Notifications\Application\StatusChanged;
use App\Notifications\Database\BackupFailed; use App\Notifications\Database\BackupFailed;
use App\Notifications\Database\BackupSuccess; use App\Notifications\Database\BackupSuccess;
use App\Notifications\Database\DailyBackup;
use App\Notifications\Test; use App\Notifications\Test;
use Exception; use Exception;
use Illuminate\Console\Command; use Illuminate\Console\Command;
@@ -54,6 +57,8 @@ class Emails extends Command
options: [ options: [
'updates' => 'Send Update Email to all users', 'updates' => 'Send Update Email to all users',
'emails-test' => 'Test', 'emails-test' => 'Test',
'database-backup-statuses-daily' => 'Database - Backup Statuses (Daily)',
'application-deployment-success-daily' => 'Application - Deployment Success (Daily)',
'application-deployment-success' => 'Application - Deployment Success', 'application-deployment-success' => 'Application - Deployment Success',
'application-deployment-failed' => 'Application - Deployment Failed', 'application-deployment-failed' => 'Application - Deployment Failed',
'application-status-changed' => 'Application - Status Changed', 'application-status-changed' => 'Application - Status Changed',
@@ -67,8 +72,12 @@ class Emails extends Command
], ],
); );
$emailsGathered = ['realusers-before-trial', 'realusers-server-lost-connection']; $emailsGathered = ['realusers-before-trial', 'realusers-server-lost-connection'];
if (!in_array($type, $emailsGathered)) { if (isDev()) {
$this->email = text('Email Address to send to'); $this->email = "test@example.com";
} else {
if (!in_array($type, $emailsGathered)) {
$this->email = text('Email Address to send to:');
}
} }
set_transanctional_email_settings(); set_transanctional_email_settings();
@@ -102,7 +111,7 @@ class Emails extends Command
$unsubscribeUrl = route('unsubscribe.marketing.emails', [ $unsubscribeUrl = route('unsubscribe.marketing.emails', [
'token' => encrypt($email), 'token' => encrypt($email),
]); ]);
$this->mail->view('emails.updates',["unsubscribeUrl" => $unsubscribeUrl]); $this->mail->view('emails.updates', ["unsubscribeUrl" => $unsubscribeUrl]);
$this->sendEmail($email); $this->sendEmail($email);
} }
} }
@@ -111,6 +120,35 @@ class Emails extends Command
$this->mail = (new Test())->toMail(); $this->mail = (new Test())->toMail();
$this->sendEmail(); $this->sendEmail();
break; break;
case 'database-backup-statuses-daily':
$scheduled_backups = ScheduledDatabaseBackup::all();
$databases = collect();
foreach ($scheduled_backups as $scheduled_backup) {
$last_days_backups = $scheduled_backup->get_last_days_backup_status();
if ($last_days_backups->isEmpty()) {
continue;
}
$failed = $last_days_backups->where('status', 'failed');
$database = $scheduled_backup->database;
$databases->put($database->name, [
'failed_count' => $failed->count(),
]);
}
$this->mail = (new DailyBackup($databases))->toMail();
$this->sendEmail();
break;
case 'application-deployment-success-daily':
$applications = Application::all();
foreach ($applications as $application) {
$deployments = $application->get_last_days_deployments();
ray($deployments);
if ($deployments->isEmpty()) {
continue;
}
$this->mail = (new DeploymentSuccess($application, 'test'))->toMail();
$this->sendEmail();
}
break;
case 'application-deployment-success': case 'application-deployment-success':
$application = Application::all()->first(); $application = Application::all()->first();
$this->mail = (new DeploymentSuccess($application, 'test'))->toMail(); $this->mail = (new DeploymentSuccess($application, 'test'))->toMail();

View File

@@ -69,12 +69,34 @@ class Kernel extends ConsoleKernel
} }
foreach ($containerServers as $server) { foreach ($containerServers as $server) {
$schedule->job(new ContainerStatusJob($server))->everyMinute()->onOneServer(); $schedule->job(new ContainerStatusJob($server))->everyMinute()->onOneServer();
// $schedule
// ->call(function () use ($server) {
// $randomSeconds = rand(1, 40);
// $job = new ContainerStatusJob($server);
// $job->delay($randomSeconds);
// ray('dispatching container status job in ' . $randomSeconds . ' seconds');
// dispatch($job);
// })->name('container-status-' . $server->id)->everyMinute()->onOneServer();
if ($server->isLogDrainEnabled()) { if ($server->isLogDrainEnabled()) {
$schedule->job(new CheckLogDrainContainerJob($server))->everyMinute()->onOneServer(); $schedule->job(new CheckLogDrainContainerJob($server))->everyMinute()->onOneServer();
// $schedule
// ->call(function () use ($server) {
// $randomSeconds = rand(1, 40);
// $job = new CheckLogDrainContainerJob($server);
// $job->delay($randomSeconds);
// dispatch($job);
// })->name('log-drain-container-check-' . $server->id)->everyMinute()->onOneServer();
} }
} }
foreach ($servers as $server) { foreach ($servers as $server) {
$schedule->job(new ServerStatusJob($server))->everyMinute()->onOneServer(); $schedule->job(new ServerStatusJob($server))->everyMinute()->onOneServer();
// $schedule
// ->call(function () use ($server) {
// $randomSeconds = rand(1, 40);
// $job = new ServerStatusJob($server);
// $job->delay($randomSeconds);
// dispatch($job);
// })->name('server-status-job-' . $server->id)->everyMinute()->onOneServer();
} }
} }
private function instance_auto_update($schedule) private function instance_auto_update($schedule)

View File

@@ -0,0 +1,186 @@
<?php
namespace App\Http\Controllers\Webhook;
use App\Http\Controllers\Controller;
use App\Livewire\Project\Service\Storage;
use App\Models\Application;
use App\Models\ApplicationPreview;
use Exception;
use Illuminate\Http\Request;
use Visus\Cuid2\Cuid2;
class Bitbucket extends Controller
{
public function manual(Request $request)
{
try {
if (app()->isDownForMaintenance()) {
ray('Maintenance mode is on');
$epoch = now()->valueOf();
$data = [
'attributes' => $request->attributes->all(),
'request' => $request->request->all(),
'query' => $request->query->all(),
'server' => $request->server->all(),
'files' => $request->files->all(),
'cookies' => $request->cookies->all(),
'headers' => $request->headers->all(),
'content' => $request->getContent(),
];
$json = json_encode($data);
Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Bitbicket::manual_bitbucket", $json);
return;
}
$return_payloads = collect([]);
$payload = $request->collect();
$headers = $request->headers->all();
$x_bitbucket_token = data_get($headers, 'x-hub-signature.0', "");
$x_bitbucket_event = data_get($headers, 'x-event-key.0', "");
$handled_events = collect(['repo:push', 'pullrequest:created', 'pullrequest:rejected', 'pullrequest:fulfilled']);
if (!$handled_events->contains($x_bitbucket_event)) {
return response([
'status' => 'failed',
'message' => 'Nothing to do. Event not handled.',
]);
}
if ($x_bitbucket_event === 'repo:push') {
$branch = data_get($payload, 'push.changes.0.new.name');
$full_name = data_get($payload, 'repository.full_name');
if (!$branch) {
return response([
'status' => 'failed',
'message' => 'Nothing to do. No branch found in the request.',
]);
}
ray('Manual webhook bitbucket push event with branch: ' . $branch);
}
if ($x_bitbucket_event === 'pullrequest:created' || $x_bitbucket_event === 'pullrequest:rejected' || $x_bitbucket_event === 'pullrequest:fulfilled') {
$branch = data_get($payload, 'pullrequest.destination.branch.name');
$base_branch = data_get($payload, 'pullrequest.source.branch.name');
$full_name = data_get($payload, 'repository.full_name');
$pull_request_id = data_get($payload, 'pullrequest.id');
$pull_request_html_url = data_get($payload, 'pullrequest.links.html.href');
$commit = data_get($payload, 'pullrequest.source.commit.hash');
}
$applications = Application::where('git_repository', 'like', "%$full_name%");
$applications = $applications->where('git_branch', $branch)->get();
if ($applications->isEmpty()) {
return response([
'status' => 'failed',
'message' => "Nothing to do. No applications found with deploy key set, branch is '$branch' and Git Repository name has $full_name.",
]);
}
foreach ($applications as $application) {
$webhook_secret = data_get($application, 'manual_webhook_secret_bitbucket');
$payload = $request->getContent();
list($algo, $hash) = explode('=', $x_bitbucket_token, 2);
$payloadHash = hash_hmac($algo, $payload, $webhook_secret);
if (!hash_equals($hash, $payloadHash) && !isDev()) {
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'Invalid token.',
]);
ray('Invalid signature');
continue;
}
$isFunctional = $application->destination->server->isFunctional();
if (!$isFunctional) {
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'Server is not functional.',
]);
ray('Server is not functional: ' . $application->destination->server->name);
continue;
}
if ($x_bitbucket_event === 'repo:push') {
if ($application->isDeployable()) {
ray('Deploying ' . $application->name . ' with branch ' . $branch);
$deployment_uuid = new Cuid2(7);
queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
force_rebuild: false,
is_webhook: true
);
$return_payloads->push([
'application' => $application->name,
'status' => 'success',
'message' => 'Preview deployment queued.',
]);
} else {
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'Auto deployment disabled.',
]);
}
}
if ($x_bitbucket_event === 'pullrequest:created') {
if ($application->isPRDeployable()) {
ray('Deploying preview for ' . $application->name . ' with branch ' . $branch . ' and base branch ' . $base_branch . ' and pull request id ' . $pull_request_id);
$deployment_uuid = new Cuid2(7);
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (!$found) {
ApplicationPreview::create([
'git_type' => 'bitbucket',
'application_id' => $application->id,
'pull_request_id' => $pull_request_id,
'pull_request_html_url' => $pull_request_html_url,
]);
}
queue_application_deployment(
application: $application,
pull_request_id: $pull_request_id,
deployment_uuid: $deployment_uuid,
force_rebuild: false,
commit: $commit,
is_webhook: true,
git_type: 'bitbucket'
);
$return_payloads->push([
'application' => $application->name,
'status' => 'success',
'message' => 'Preview deployment queued.',
]);
} else {
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'Preview deployments disabled.',
]);
}
}
if ($x_bitbucket_event === 'pullrequest:rejected' || $x_bitbucket_event === 'pullrequest:fulfilled') {
ray('Pull request rejected');
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if ($found) {
$found->delete();
$container_name = generateApplicationContainerName($application, $pull_request_id);
instant_remote_process(["docker rm -f $container_name"], $application->destination->server);
$return_payloads->push([
'application' => $application->name,
'status' => 'success',
'message' => 'Preview deployment closed.',
]);
} else {
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'No preview deployment found.',
]);
}
}
}
ray($return_payloads);
return response($return_payloads);
} catch (Exception $e) {
ray($e);
return handleError($e);
}
}
}

View File

@@ -0,0 +1,459 @@
<?php
namespace App\Http\Controllers\Webhook;
use App\Enums\ProcessStatus;
use App\Http\Controllers\Controller;
use App\Jobs\ApplicationPullRequestUpdateJob;
use App\Jobs\GithubAppPermissionJob;
use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Models\GithubApp;
use App\Models\PrivateKey;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Visus\Cuid2\Cuid2;
class Github extends Controller
{
public function manual(Request $request)
{
try {
ray($request);
$return_payloads = collect([]);
$x_github_delivery = request()->header('X-GitHub-Delivery');
if (app()->isDownForMaintenance()) {
ray('Maintenance mode is on');
$epoch = now()->valueOf();
$files = Storage::disk('webhooks-during-maintenance')->files();
$github_delivery_found = collect($files)->filter(function ($file) use ($x_github_delivery) {
return Str::contains($file, $x_github_delivery);
})->first();
if ($github_delivery_found) {
ray('Webhook already found');
return;
}
$data = [
'attributes' => $request->attributes->all(),
'request' => $request->request->all(),
'query' => $request->query->all(),
'server' => $request->server->all(),
'files' => $request->files->all(),
'cookies' => $request->cookies->all(),
'headers' => $request->headers->all(),
'content' => $request->getContent(),
];
$json = json_encode($data);
Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Github::manual_{$x_github_delivery}", $json);
return;
}
$x_github_event = Str::lower($request->header('X-GitHub-Event'));
$x_hub_signature_256 = Str::after($request->header('X-Hub-Signature-256'), 'sha256=');
$content_type = $request->header('Content-Type');
$payload = $request->collect();
if ($x_github_event === 'ping') {
// Just pong
return response('pong');
}
if ($content_type !== 'application/json') {
$payload = json_decode(data_get($payload, 'payload'), true);
}
if ($x_github_event === 'push') {
$branch = data_get($payload, 'ref');
$full_name = data_get($payload, 'repository.full_name');
if (Str::isMatch('/refs\/heads\/*/', $branch)) {
$branch = Str::after($branch, 'refs/heads/');
}
ray('Manual Webhook GitHub Push Event with branch: ' . $branch);
}
if ($x_github_event === 'pull_request') {
$action = data_get($payload, 'action');
$full_name = data_get($payload, 'repository.full_name');
$pull_request_id = data_get($payload, 'number');
$pull_request_html_url = data_get($payload, 'pull_request.html_url');
$branch = data_get($payload, 'pull_request.head.ref');
$base_branch = data_get($payload, 'pull_request.base.ref');
ray('Webhook GitHub Pull Request Event with branch: ' . $branch . ' and base branch: ' . $base_branch . ' and pull request id: ' . $pull_request_id);
}
if (!$branch) {
return response('Nothing to do. No branch found in the request.');
}
$applications = Application::where('git_repository', 'like', "%$full_name%");
if ($x_github_event === 'push') {
$applications = $applications->where('git_branch', $branch)->get();
if ($applications->isEmpty()) {
return response("Nothing to do. No applications found with deploy key set, branch is '$branch' and Git Repository name has $full_name.");
}
}
if ($x_github_event === 'pull_request') {
$applications = $applications->where('git_branch', $base_branch)->get();
if ($applications->isEmpty()) {
return response("Nothing to do. No applications found with branch '$base_branch'.");
}
}
foreach ($applications as $application) {
$webhook_secret = data_get($application, 'manual_webhook_secret_github');
$hmac = hash_hmac('sha256', $request->getContent(), $webhook_secret);
if (!hash_equals($x_hub_signature_256, $hmac) && !isDev()) {
ray('Invalid signature');
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'Invalid token.',
]);
continue;
}
$isFunctional = $application->destination->server->isFunctional();
if (!$isFunctional) {
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'Server is not functional.',
]);
continue;
}
if ($x_github_event === 'push') {
if ($application->isDeployable()) {
ray('Deploying ' . $application->name . ' with branch ' . $branch);
$deployment_uuid = new Cuid2(7);
queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
force_rebuild: false,
is_webhook: true,
);
$return_payloads->push([
'application' => $application->name,
'status' => 'success',
'message' => 'Deployment queued.',
]);
} else {
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'Deployments disabled.',
]);
}
}
if ($x_github_event === 'pull_request') {
if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') {
if ($application->isPRDeployable()) {
$deployment_uuid = new Cuid2(7);
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (!$found) {
ApplicationPreview::create([
'git_type' => 'github',
'application_id' => $application->id,
'pull_request_id' => $pull_request_id,
'pull_request_html_url' => $pull_request_html_url,
]);
}
queue_application_deployment(
application: $application,
pull_request_id: $pull_request_id,
deployment_uuid: $deployment_uuid,
force_rebuild: false,
is_webhook: true,
git_type: 'github'
);
$return_payloads->push([
'application' => $application->name,
'status' => 'success',
'message' => 'Preview deployment queued.',
]);
} else {
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'Preview deployments disabled.',
]);
}
}
if ($action === 'closed') {
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if ($found) {
$found->delete();
$container_name = generateApplicationContainerName($application, $pull_request_id);
// ray('Stopping container: ' . $container_name);
instant_remote_process(["docker rm -f $container_name"], $application->destination->server);
$return_payloads->push([
'application' => $application->name,
'status' => 'success',
'message' => 'Preview deployment closed.',
]);
} else {
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'No preview deployment found.',
]);
}
}
}
}
ray($return_payloads);
return response($return_payloads);
} catch (Exception $e) {
ray($e->getMessage());
return handleError($e);
}
}
public function normal(Request $request)
{
try {
$return_payloads = collect([]);
$id = null;
$x_github_delivery = $request->header('X-GitHub-Delivery');
if (app()->isDownForMaintenance()) {
ray('Maintenance mode is on');
$epoch = now()->valueOf();
$files = Storage::disk('webhooks-during-maintenance')->files();
$github_delivery_found = collect($files)->filter(function ($file) use ($x_github_delivery) {
return Str::contains($file, $x_github_delivery);
})->first();
if ($github_delivery_found) {
ray('Webhook already found');
return;
}
$data = [
'attributes' => $request->attributes->all(),
'request' => $request->request->all(),
'query' => $request->query->all(),
'server' => $request->server->all(),
'files' => $request->files->all(),
'cookies' => $request->cookies->all(),
'headers' => $request->headers->all(),
'content' => $request->getContent(),
];
$json = json_encode($data);
Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Github::normal_{$x_github_delivery}", $json);
return;
}
$x_github_event = Str::lower($request->header('X-GitHub-Event'));
$x_github_hook_installation_target_id = $request->header('X-GitHub-Hook-Installation-Target-Id');
$x_hub_signature_256 = Str::after($request->header('X-Hub-Signature-256'), 'sha256=');
$payload = $request->collect();
if ($x_github_event === 'ping') {
// Just pong
return response('pong');
}
$github_app = GithubApp::where('app_id', $x_github_hook_installation_target_id)->first();
if (is_null($github_app)) {
return response('Nothing to do. No GitHub App found.');
}
$webhook_secret = data_get($github_app, 'webhook_secret');
$hmac = hash_hmac('sha256', $request->getContent(), $webhook_secret);
if (config('app.env') !== 'local') {
if (!hash_equals($x_hub_signature_256, $hmac)) {
return response('Invalid signature.');
}
}
if ($x_github_event === 'installation' || $x_github_event === 'installation_repositories') {
// Installation handled by setup redirect url. Repositories queried on-demand.
$action = data_get($payload, 'action');
if ($action === 'new_permissions_accepted') {
GithubAppPermissionJob::dispatch($github_app);
}
return response('cool');
}
if ($x_github_event === 'push') {
$id = data_get($payload, 'repository.id');
$branch = data_get($payload, 'ref');
if (Str::isMatch('/refs\/heads\/*/', $branch)) {
$branch = Str::after($branch, 'refs/heads/');
}
ray('Webhook GitHub Push Event: ' . $id . ' with branch: ' . $branch);
}
if ($x_github_event === 'pull_request') {
$action = data_get($payload, 'action');
$id = data_get($payload, 'repository.id');
$pull_request_id = data_get($payload, 'number');
$pull_request_html_url = data_get($payload, 'pull_request.html_url');
$branch = data_get($payload, 'pull_request.head.ref');
$base_branch = data_get($payload, 'pull_request.base.ref');
ray('Webhook GitHub Pull Request Event: ' . $id . ' with branch: ' . $branch . ' and base branch: ' . $base_branch . ' and pull request id: ' . $pull_request_id);
}
if (!$id || !$branch) {
return response('Nothing to do. No id or branch found.');
}
$applications = Application::where('repository_project_id', $id)->whereRelation('source', 'is_public', false);
if ($x_github_event === 'push') {
$applications = $applications->where('git_branch', $branch)->get();
if ($applications->isEmpty()) {
return response("Nothing to do. No applications found with branch '$branch'.");
}
}
if ($x_github_event === 'pull_request') {
$applications = $applications->where('git_branch', $base_branch)->get();
if ($applications->isEmpty()) {
return response("Nothing to do. No applications found with branch '$base_branch'.");
}
}
foreach ($applications as $application) {
$isFunctional = $application->destination->server->isFunctional();
if (!$isFunctional) {
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'Server is not functional.',
]);
continue;
}
if ($x_github_event === 'push') {
if ($application->isDeployable()) {
ray('Deploying ' . $application->name . ' with branch ' . $branch);
$deployment_uuid = new Cuid2(7);
queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
force_rebuild: false,
is_webhook: true
);
$return_payloads->push([
'application' => $application->name,
'status' => 'success',
'message' => 'Deployment queued.',
]);
} else {
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'Deployments disabled.',
]);
}
}
if ($x_github_event === 'pull_request') {
if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') {
if ($application->isPRDeployable()) {
$deployment_uuid = new Cuid2(7);
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (!$found) {
ApplicationPreview::create([
'git_type' => 'github',
'application_id' => $application->id,
'pull_request_id' => $pull_request_id,
'pull_request_html_url' => $pull_request_html_url,
]);
}
queue_application_deployment(
application: $application,
pull_request_id: $pull_request_id,
deployment_uuid: $deployment_uuid,
force_rebuild: false,
is_webhook: true,
git_type: 'github'
);
$return_payloads->push([
'application' => $application->name,
'status' => 'success',
'message' => 'Preview deployment queued.',
]);
} else {
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'Preview deployments disabled.',
]);
}
}
if ($action === 'closed' || $action === 'close') {
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if ($found) {
ApplicationPullRequestUpdateJob::dispatchSync(application: $application, preview: $found, status: ProcessStatus::CLOSED);
$found->delete();
$container_name = generateApplicationContainerName($application, $pull_request_id);
// ray('Stopping container: ' . $container_name);
instant_remote_process(["docker rm -f $container_name"], $application->destination->server);
$return_payloads->push([
'application' => $application->name,
'status' => 'success',
'message' => 'Preview deployment closed.',
]);
} else {
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'No preview deployment found.',
]);
}
}
}
}
ray($return_payloads);
return response($return_payloads);
} catch (Exception $e) {
ray($e->getMessage());
return handleError($e);
}
}
public function redirect(Request $request)
{
try {
$code = $request->get('code');
$state = $request->get('state');
$github_app = GithubApp::where('uuid', $state)->firstOrFail();
$api_url = data_get($github_app, 'api_url');
$data = Http::withBody(null)->accept('application/vnd.github+json')->post("$api_url/app-manifests/$code/conversions")->throw()->json();
$id = data_get($data, 'id');
$slug = data_get($data, 'slug');
$client_id = data_get($data, 'client_id');
$client_secret = data_get($data, 'client_secret');
$private_key = data_get($data, 'pem');
$webhook_secret = data_get($data, 'webhook_secret');
$private_key = PrivateKey::create([
'name' => $slug,
'private_key' => $private_key,
'team_id' => $github_app->team_id,
'is_git_related' => true,
]);
$github_app->name = $slug;
$github_app->app_id = $id;
$github_app->client_id = $client_id;
$github_app->client_secret = $client_secret;
$github_app->webhook_secret = $webhook_secret;
$github_app->private_key_id = $private_key->id;
$github_app->save();
return redirect()->route('source.github.show', ['github_app_uuid' => $github_app->uuid]);
} catch (Exception $e) {
return handleError($e);
}
}
public function install(Request $request)
{
try {
$installation_id = $request->get('installation_id');
if (app()->isDownForMaintenance()) {
ray('Maintenance mode is on');
$epoch = now()->valueOf();
$data = [
'attributes' => $request->attributes->all(),
'request' => $request->request->all(),
'query' => $request->query->all(),
'server' => $request->server->all(),
'files' => $request->files->all(),
'cookies' => $request->cookies->all(),
'headers' => $request->headers->all(),
'content' => $request->getContent(),
];
$json = json_encode($data);
Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Github::install_{$installation_id}", $json);
return;
}
$source = $request->get('source');
$setup_action = $request->get('setup_action');
$github_app = GithubApp::where('uuid', $source)->firstOrFail();
if ($setup_action === 'install') {
$github_app->installation_id = $installation_id;
$github_app->save();
}
return redirect()->route('source.github.show', ['github_app_uuid' => $github_app->uuid]);
} catch (Exception $e) {
return handleError($e);
}
}
}

View File

@@ -0,0 +1,202 @@
<?php
namespace App\Http\Controllers\Webhook;
use App\Http\Controllers\Controller;
use App\Models\Application;
use App\Models\ApplicationPreview;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Visus\Cuid2\Cuid2;
class Gitlab extends Controller
{
public function manual(Request $request)
{
try {
if (app()->isDownForMaintenance()) {
ray('Maintenance mode is on');
$epoch = now()->valueOf();
$data = [
'attributes' => $request->attributes->all(),
'request' => $request->request->all(),
'query' => $request->query->all(),
'server' => $request->server->all(),
'files' => $request->files->all(),
'cookies' => $request->cookies->all(),
'headers' => $request->headers->all(),
'content' => $request->getContent(),
];
$json = json_encode($data);
Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Gitlab::manual_gitlab", $json);
return;
}
$return_payloads = collect([]);
$payload = $request->collect();
$headers = $request->headers->all();
$x_gitlab_token = data_get($headers, 'x-gitlab-token.0');
$x_gitlab_event = data_get($payload, 'object_kind');
if ($x_gitlab_event === 'push') {
$branch = data_get($payload, 'ref');
$full_name = data_get($payload, 'project.path_with_namespace');
if (Str::isMatch('/refs\/heads\/*/', $branch)) {
$branch = Str::after($branch, 'refs/heads/');
}
if (!$branch) {
$return_payloads->push([
'status' => 'failed',
'message' => 'Nothing to do. No branch found in the request.',
]);
return response($return_payloads);
}
ray('Manual Webhook GitLab Push Event with branch: ' . $branch);
}
if ($x_gitlab_event === 'merge_request') {
$action = data_get($payload, 'object_attributes.action');
$branch = data_get($payload, 'object_attributes.source_branch');
$base_branch = data_get($payload, 'object_attributes.target_branch');
$full_name = data_get($payload, 'project.path_with_namespace');
$pull_request_id = data_get($payload, 'object_attributes.iid');
$pull_request_html_url = data_get($payload, 'object_attributes.url');
if (!$branch) {
$return_payloads->push([
'status' => 'failed',
'message' => 'Nothing to do. No branch found in the request.',
]);
return response($return_payloads);
}
ray('Webhook GitHub Pull Request Event with branch: ' . $branch . ' and base branch: ' . $base_branch . ' and pull request id: ' . $pull_request_id);
}
$applications = Application::where('git_repository', 'like', "%$full_name%");
if ($x_gitlab_event === 'push') {
$applications = $applications->where('git_branch', $branch)->get();
if ($applications->isEmpty()) {
$return_payloads->push([
'status' => 'failed',
'message' => "Nothing to do. No applications found with deploy key set, branch is '$branch' and Git Repository name has $full_name.",
]);
return response($return_payloads);
}
}
if ($x_gitlab_event === 'merge_request') {
$applications = $applications->where('git_branch', $base_branch)->get();
if ($applications->isEmpty()) {
$return_payloads->push([
'status' => 'failed',
'message' => "Nothing to do. No applications found with branch '$base_branch'.",
]);
return response($return_payloads);
}
}
foreach ($applications as $application) {
$webhook_secret = data_get($application, 'manual_webhook_secret_gitlab');
if ($webhook_secret !== $x_gitlab_token) {
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'Invalid token.',
]);
ray('Invalid signature');
continue;
}
$isFunctional = $application->destination->server->isFunctional();
if (!$isFunctional) {
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'Server is not functional',
]);
ray('Server is not functional: ' . $application->destination->server->name);
continue;
}
if ($x_gitlab_event === 'push') {
if ($application->isDeployable()) {
ray('Deploying ' . $application->name . ' with branch ' . $branch);
$deployment_uuid = new Cuid2(7);
queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
force_rebuild: false,
is_webhook: true
);
} else {
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'Deployments disabled',
]);
ray('Deployments disabled for ' . $application->name);
}
}
if ($x_gitlab_event === 'merge_request') {
if ($action === 'open' || $action === 'opened' || $action === 'synchronize' || $action === 'reopened' || $action === 'reopen' || $action === 'update') {
if ($application->isPRDeployable()) {
$deployment_uuid = new Cuid2(7);
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (!$found) {
ApplicationPreview::create([
'git_type' => 'gitlab',
'application_id' => $application->id,
'pull_request_id' => $pull_request_id,
'pull_request_html_url' => $pull_request_html_url,
]);
}
queue_application_deployment(
application: $application,
pull_request_id: $pull_request_id,
deployment_uuid: $deployment_uuid,
force_rebuild: false,
is_webhook: true,
git_type: 'gitlab'
);
ray('Deploying preview for ' . $application->name . ' with branch ' . $branch . ' and base branch ' . $base_branch . ' and pull request id ' . $pull_request_id);
$return_payloads->push([
'application' => $application->name,
'status' => 'success',
'message' => 'Preview Deployment queued',
]);
} else {
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'Preview deployments disabled',
]);
ray('Preview deployments disabled for ' . $application->name);
}
} else if ($action === 'closed' || $action === 'close') {
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if ($found) {
$found->delete();
$container_name = generateApplicationContainerName($application, $pull_request_id);
// ray('Stopping container: ' . $container_name);
instant_remote_process(["docker rm -f $container_name"], $application->destination->server);
$return_payloads->push([
'application' => $application->name,
'status' => 'success',
'message' => 'Preview Deployment closed',
]);
return response($return_payloads);
}
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'No Preview Deployment found',
]);
} else {
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'No action found. Contact us for debugging.',
]);
}
}
}
return response($return_payloads);
} catch (Exception $e) {
ray($e->getMessage());
return handleError($e);
}
}
}

View File

@@ -0,0 +1,258 @@
<?php
namespace App\Http\Controllers\Webhook;
use App\Http\Controllers\Controller;
use App\Jobs\ServerLimitCheckJob;
use App\Jobs\SubscriptionInvoiceFailedJob;
use App\Jobs\SubscriptionTrialEndedJob;
use App\Jobs\SubscriptionTrialEndsSoonJob;
use App\Models\Subscription;
use App\Models\Team;
use App\Models\Webhook;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Sleep;
use Illuminate\Support\Str;
class Stripe extends Controller
{
public function events(Request $request)
{
try {
if (app()->isDownForMaintenance()) {
ray('Maintenance mode is on');
$epoch = now()->valueOf();
$data = [
'attributes' => $request->attributes->all(),
'request' => $request->request->all(),
'query' => $request->query->all(),
'server' => $request->server->all(),
'files' => $request->files->all(),
'cookies' => $request->cookies->all(),
'headers' => $request->headers->all(),
'content' => $request->getContent(),
];
$json = json_encode($data);
Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Stripe::events_stripe", $json);
return;
}
$webhookSecret = config('subscription.stripe_webhook_secret');
$signature = $request->header('Stripe-Signature');
$excludedPlans = config('subscription.stripe_excluded_plans');
$event = \Stripe\Webhook::constructEvent(
$request->getContent(),
$signature,
$webhookSecret
);
$webhook = Webhook::create([
'type' => 'stripe',
'payload' => $request->getContent()
]);
$type = data_get($event, 'type');
$data = data_get($event, 'data.object');
switch ($type) {
case 'checkout.session.completed':
$clientReferenceId = data_get($data, 'client_reference_id');
if (is_null($clientReferenceId)) {
send_internal_notification('Checkout session completed without client reference id.');
break;
}
$userId = Str::before($clientReferenceId, ':');
$teamId = Str::after($clientReferenceId, ':');
$subscriptionId = data_get($data, 'subscription');
$customerId = data_get($data, 'customer');
$team = Team::find($teamId);
$found = $team->members->where('id', $userId)->first();
if (!$found->isAdmin()) {
send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
throw new Exception("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
}
$subscription = Subscription::where('team_id', $teamId)->first();
if ($subscription) {
send_internal_notification('Old subscription activated for team: ' . $teamId);
$subscription->update([
'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => true,
]);
} else {
send_internal_notification('New subscription for team: ' . $teamId);
Subscription::create([
'team_id' => $teamId,
'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => true,
]);
}
break;
case 'invoice.paid':
$customerId = data_get($data, 'customer');
$planId = data_get($data, 'lines.data.0.plan.id');
if (Str::contains($excludedPlans, $planId)) {
send_internal_notification('Subscription excluded.');
break;
}
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if (!$subscription) {
Sleep::for(5)->seconds();
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
}
$subscription->update([
'stripe_invoice_paid' => true,
]);
break;
case 'invoice.payment_failed':
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if (!$subscription) {
send_internal_notification('invoice.payment_failed failed but no subscription found in Coolify for customer: ' . $customerId);
return response('No subscription found in Coolify.');
}
$team = data_get($subscription, 'team');
if (!$team) {
send_internal_notification('invoice.payment_failed failed but no team found in Coolify for customer: ' . $customerId);
return response('No team found in Coolify.');
}
if (!$subscription->stripe_invoice_paid) {
SubscriptionInvoiceFailedJob::dispatch($team);
send_internal_notification('Invoice payment failed: ' . $customerId);
} else {
send_internal_notification('Invoice payment failed but already paid: ' . $customerId);
}
break;
case 'payment_intent.payment_failed':
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if (!$subscription) {
send_internal_notification('payment_intent.payment_failed, no subscription found in Coolify for customer: ' . $customerId);
return response('No subscription found in Coolify.');
}
if ($subscription->stripe_invoice_paid) {
send_internal_notification('payment_intent.payment_failed but invoice is active for customer: ' . $customerId);
return;
}
send_internal_notification('Subscription payment failed for customer: ' . $customerId);
break;
case 'customer.subscription.updated':
$customerId = data_get($data, 'customer');
$status = data_get($data, 'status');
$subscriptionId = data_get($data, 'items.data.0.subscription');
$planId = data_get($data, 'items.data.0.plan.id');
if (Str::contains($excludedPlans, $planId)) {
send_internal_notification('Subscription excluded.');
break;
}
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if (!$subscription) {
Sleep::for(5)->seconds();
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
}
if (!$subscription) {
send_internal_notification('No subscription found for: ' . $customerId);
return response("No subscription found", 400);
}
$trialEndedAlready = data_get($subscription, 'stripe_trial_already_ended');
$cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end');
$alreadyCancelAtPeriodEnd = data_get($subscription, 'stripe_cancel_at_period_end');
$feedback = data_get($data, 'cancellation_details.feedback');
$comment = data_get($data, 'cancellation_details.comment');
$lookup_key = data_get($data, 'items.data.0.price.lookup_key');
if (str($lookup_key)->contains('ultimate')) {
$quantity = data_get($data, 'items.data.0.quantity', 10);
$team = data_get($subscription, 'team');
$team->update([
'custom_server_limit' => $quantity,
]);
ServerLimitCheckJob::dispatch($team);
}
$subscription->update([
'stripe_feedback' => $feedback,
'stripe_comment' => $comment,
'stripe_plan_id' => $planId,
'stripe_cancel_at_period_end' => $cancelAtPeriodEnd,
]);
if ($status === 'paused' || $status === 'incomplete_expired') {
$subscription->update([
'stripe_invoice_paid' => false,
]);
send_internal_notification('Subscription paused or incomplete for customer: ' . $customerId);
}
// Trial ended but subscribed, reactive servers
if ($trialEndedAlready && $status === 'active') {
$team = data_get($subscription, 'team');
$team->trialEndedButSubscribed();
}
if ($feedback) {
$reason = "Cancellation feedback for {$customerId}: '" . $feedback . "'";
if ($comment) {
$reason .= ' with comment: \'' . $comment . "'";
}
send_internal_notification($reason);
}
if ($alreadyCancelAtPeriodEnd !== $cancelAtPeriodEnd) {
if ($cancelAtPeriodEnd) {
// send_internal_notification('Subscription cancelled at period end for team: ' . $subscription->team->id);
} else {
send_internal_notification('customer.subscription.updated for customer: ' . $customerId);
}
}
break;
case 'customer.subscription.deleted':
// End subscription
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
$team = data_get($subscription, 'team');
$team->trialEnded();
$subscription->update([
'stripe_subscription_id' => null,
'stripe_plan_id' => null,
'stripe_cancel_at_period_end' => false,
'stripe_invoice_paid' => false,
'stripe_trial_already_ended' => true,
]);
send_internal_notification('customer.subscription.deleted for customer: ' . $customerId);
break;
case 'customer.subscription.trial_will_end':
// Not used for now
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
$team = data_get($subscription, 'team');
if (!$team) {
throw new Exception('No team found for subscription: ' . $subscription->id);
}
SubscriptionTrialEndsSoonJob::dispatch($team);
break;
case 'customer.subscription.paused':
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
$team = data_get($subscription, 'team');
if (!$team) {
throw new Exception('No team found for subscription: ' . $subscription->id);
}
$team->trialEnded();
$subscription->update([
'stripe_trial_already_ended' => true,
'stripe_invoice_paid' => false,
]);
SubscriptionTrialEndedJob::dispatch($team);
send_internal_notification('Subscription paused for customer: ' . $customerId);
break;
default:
// Unhandled event type
}
} catch (Exception $e) {
if ($type !== 'payment_intent.payment_failed') {
send_internal_notification("Subscription webhook ($type) failed: " . $e->getMessage());
}
$webhook->update([
'status' => 'failed',
'failure_reason' => $e->getMessage(),
]);
return response($e->getMessage(), 400);
}
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace App\Http\Controllers\Webhook;
use App\Http\Controllers\Controller;
use App\Models\Waitlist as ModelsWaitlist;
use Exception;
use Illuminate\Http\Request;
class Waitlist extends Controller
{
public function confirm(Request $request)
{
$email = request()->get('email');
$confirmation_code = request()->get('confirmation_code');
ray($email, $confirmation_code);
try {
$found = ModelsWaitlist::where('uuid', $confirmation_code)->where('email', $email)->first();
if ($found) {
if (!$found->verified) {
if ($found->created_at > now()->subMinutes(config('constants.waitlist.expiration'))) {
$found->verified = true;
$found->save();
send_internal_notification('Waitlist confirmed: ' . $email);
return 'Thank you for confirming your email address. We will notify you when you are next in line.';
} else {
$found->delete();
send_internal_notification('Waitlist expired: ' . $email);
return 'Your confirmation code has expired. Please sign up again.';
}
}
}
return redirect()->route('dashboard');
} catch (Exception $e) {
send_internal_notification('Waitlist confirmation failed: ' . $e->getMessage());
ray($e->getMessage());
return redirect()->route('dashboard');
}
}
public function cancel(Request $request)
{
$email = request()->get('email');
$confirmation_code = request()->get('confirmation_code');
try {
$found = ModelsWaitlist::where('uuid', $confirmation_code)->where('email', $email)->first();
if ($found && !$found->verified) {
$found->delete();
send_internal_notification('Waitlist cancelled: ' . $email);
return 'Your email address has been removed from the waitlist.';
}
return redirect()->route('dashboard');
} catch (Exception $e) {
send_internal_notification('Waitlist cancellation failed: ' . $e->getMessage());
ray($e->getMessage());
return redirect()->route('dashboard');
}
}
}

View File

@@ -12,6 +12,6 @@ class PreventRequestsDuringMaintenance extends Middleware
* @var array<int, string> * @var array<int, string>
*/ */
protected $except = [ protected $except = [
// 'webhooks/*',
]; ];
} }

View File

@@ -167,65 +167,71 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($this->application->is_github_based()) { if ($this->application->is_github_based()) {
ApplicationPullRequestUpdateJob::dispatch(application: $this->application, preview: $this->preview, deployment_uuid: $this->deployment_uuid, status: ProcessStatus::IN_PROGRESS); ApplicationPullRequestUpdateJob::dispatch(application: $this->application, preview: $this->preview, deployment_uuid: $this->deployment_uuid, status: ProcessStatus::IN_PROGRESS);
} }
if ($this->application->build_pack === 'dockerfile') {
if (data_get($this->application, 'dockerfile_location')) {
$this->dockerfile_location = $this->application->dockerfile_location;
}
}
} }
} }
public function handle(): void public function handle(): void
{ {
// Generate custom host<->ip mapping try {
$allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server); // Generate custom host<->ip mapping
if (!is_null($allContainers)) { $allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server);
$allContainers = format_docker_command_output_to_json($allContainers);
$ips = collect([]); if (!is_null($allContainers)) {
if (count($allContainers) > 0) { $allContainers = format_docker_command_output_to_json($allContainers);
$allContainers = $allContainers[0]; $ips = collect([]);
$allContainers = collect($allContainers)->sort()->values(); if (count($allContainers) > 0) {
foreach ($allContainers as $container) { $allContainers = $allContainers[0];
$containerName = data_get($container, 'Name'); $allContainers = collect($allContainers)->sort()->values();
if ($containerName === 'coolify-proxy') { foreach ($allContainers as $container) {
continue; $containerName = data_get($container, 'Name');
} if ($containerName === 'coolify-proxy') {
if (preg_match('/-(\d{12})/', $containerName)) { continue;
continue; }
} if (preg_match('/-(\d{12})/', $containerName)) {
$containerIp = data_get($container, 'IPv4Address'); continue;
if ($containerName && $containerIp) { }
$containerIp = str($containerIp)->before('/'); $containerIp = data_get($container, 'IPv4Address');
$ips->put($containerName, $containerIp->value()); if ($containerName && $containerIp) {
$containerIp = str($containerIp)->before('/');
$ips->put($containerName, $containerIp->value());
}
} }
} }
$this->addHosts = $ips->map(function ($ip, $name) {
return "--add-host $name:$ip";
})->implode(' ');
} }
$this->addHosts = $ips->map(function ($ip, $name) {
return "--add-host $name:$ip";
})->implode(' ');
}
if ($this->application->dockerfile_target_build) { if ($this->application->dockerfile_target_build) {
$this->buildTarget = " --target {$this->application->dockerfile_target_build} "; $this->buildTarget = " --target {$this->application->dockerfile_target_build} ";
} }
// Check custom port // Check custom port
['repository' => $this->customRepository, 'port' => $this->customPort] = $this->application->customRepository(); ['repository' => $this->customRepository, 'port' => $this->customPort] = $this->application->customRepository();
if (data_get($this->application, 'settings.is_build_server_enabled')) { if (data_get($this->application, 'settings.is_build_server_enabled')) {
$teamId = data_get($this->application, 'environment.project.team.id'); $teamId = data_get($this->application, 'environment.project.team.id');
$buildServers = Server::buildServers($teamId)->get(); $buildServers = Server::buildServers($teamId)->get();
if ($buildServers->count() === 0) { if ($buildServers->count() === 0) {
$this->application_deployment_queue->addLogEntry("Build server feature activated, but no suitable build server found. Using the deployment server."); $this->application_deployment_queue->addLogEntry("No suitable build server found. Using the deployment server.");
$this->build_server = $this->server;
$this->original_server = $this->server;
} else {
$this->build_server = $buildServers->random();
$this->application_deployment_queue->addLogEntry("Found a suitable build server ({$this->build_server->name}).");
$this->original_server = $this->server;
$this->use_build_server = true;
}
} else {
// Set build server & original_server to the same as deployment server
$this->build_server = $this->server; $this->build_server = $this->server;
$this->original_server = $this->server; $this->original_server = $this->server;
} else {
$this->application_deployment_queue->addLogEntry("Build server feature activated and found a suitable build server. Using it to build your application - if needed.");
$this->build_server = $buildServers->random();
$this->original_server = $this->server;
$this->use_build_server = true;
} }
} else {
// Set build server & original_server to the same as deployment server
$this->build_server = $this->server;
$this->original_server = $this->server;
}
try {
if ($this->restart_only && $this->application->build_pack !== 'dockerimage') { if ($this->restart_only && $this->application->build_pack !== 'dockerimage') {
$this->just_restart(); $this->just_restart();
if ($this->server->isProxyShouldRun()) { if ($this->server->isProxyShouldRun()) {
@@ -421,13 +427,13 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} }
private function deploy_dockerfile_buildpack() private function deploy_dockerfile_buildpack()
{ {
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch} to {$this->server->name}.");
if ($this->use_build_server) { if ($this->use_build_server) {
$this->server = $this->build_server; $this->server = $this->build_server;
} }
if (data_get($this->application, 'dockerfile_location')) { if (data_get($this->application, 'dockerfile_location')) {
$this->dockerfile_location = $this->application->dockerfile_location; $this->dockerfile_location = $this->application->dockerfile_location;
} }
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch} to {$this->server->name}.");
$this->prepare_builder_image(); $this->prepare_builder_image();
$this->check_git_if_build_needed(); $this->check_git_if_build_needed();
$this->set_base_dir(); $this->set_base_dir();
@@ -522,9 +528,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->server = $this->original_server; $this->server = $this->original_server;
} }
$readme = generate_readme_file($this->application->name, $this->application_deployment_queue->updated_at); $readme = generate_readme_file($this->application->name, $this->application_deployment_queue->updated_at);
$composeFileName = "$this->configuration_dir/docker-compose.yml"; if ($this->pull_request_id === 0) {
if ($this->pull_request_id !== 0) { $composeFileName = "$this->configuration_dir/docker-compose.yml";
} else {
$composeFileName = "$this->configuration_dir/docker-compose-pr-{$this->pull_request_id}.yml"; $composeFileName = "$this->configuration_dir/docker-compose-pr-{$this->pull_request_id}.yml";
$this->docker_compose_location = "/docker-compose-pr-{$this->pull_request_id}.yml";
} }
$this->execute_remote_command( $this->execute_remote_command(
[ [
@@ -719,7 +727,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->write_deployment_configurations(); $this->write_deployment_configurations();
$this->server = $this->original_server; $this->server = $this->original_server;
} }
if (count($this->application->ports_mappings_array) > 0 || (bool) $this->application->settings->is_consistent_container_name_enabled) { if (count($this->application->ports_mappings_array) > 0 || (bool) $this->application->settings->is_consistent_container_name_enabled || $this->pull_request_id !== 0) {
$this->application_deployment_queue->addLogEntry("----------------------------------------"); $this->application_deployment_queue->addLogEntry("----------------------------------------");
if (count($this->application->ports_mappings_array) > 0) { if (count($this->application->ports_mappings_array) > 0) {
$this->application_deployment_queue->addLogEntry("Application has ports mapped to the host system, rolling update is not supported."); $this->application_deployment_queue->addLogEntry("Application has ports mapped to the host system, rolling update is not supported.");
@@ -727,6 +735,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ((bool) $this->application->settings->is_consistent_container_name_enabled) { if ((bool) $this->application->settings->is_consistent_container_name_enabled) {
$this->application_deployment_queue->addLogEntry("Consistent container name feature enabled, rolling update is not supported."); $this->application_deployment_queue->addLogEntry("Consistent container name feature enabled, rolling update is not supported.");
} }
if ($this->pull_request_id !== 0) {
$this->application->settings->is_consistent_container_name_enabled = true;
$this->application_deployment_queue->addLogEntry("Pull request deployment, rolling update is not supported.");
}
$this->stop_running_container(force: true); $this->stop_running_container(force: true);
$this->start_by_compose_file(); $this->start_by_compose_file();
} else { } else {
@@ -804,26 +816,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->add_build_env_variables_to_dockerfile(); $this->add_build_env_variables_to_dockerfile();
} }
$this->build_image(); $this->build_image();
$this->stop_running_container(); $this->push_to_docker_registry();
if ($this->application->destination->server->isSwarm()) { // $this->stop_running_container();
$this->push_to_docker_registry(); $this->rolling_update();
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "docker stack deploy --with-registry-auth -c {$this->workdir}{$this->docker_compose_location} {$this->application->uuid}-{$this->pull_request_id}")
],
);
} else {
$this->application_deployment_queue->addLogEntry("Starting preview deployment.");
if ($this->use_build_server) {
$this->execute_remote_command(
["SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->configuration_dir} -f {$this->configuration_dir}{$this->docker_compose_location} up --build -d", "hidden" => true],
);
} else {
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up --build -d"), "hidden" => true],
);
}
}
} }
private function create_workdir() private function create_workdir()
{ {
@@ -1220,17 +1215,45 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
// ]; // ];
// } // }
if ((bool)$this->application->settings->is_consistent_container_name_enabled) { if ($this->pull_request_id === 0) {
$custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options); if ((bool)$this->application->settings->is_consistent_container_name_enabled) {
if (count($custom_compose) > 0) { $custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options);
$docker_compose['services'][$this->container_name] = array_merge_recursive($docker_compose['services'][$this->container_name], $custom_compose); if (count($custom_compose) > 0) {
} $ipv4 = data_get($custom_compose, 'ip.0');
} else { $ipv6 = data_get($custom_compose, 'ip6.0');
$docker_compose['services'][$this->application->uuid] = $docker_compose['services'][$this->container_name]; data_forget($custom_compose, 'ip');
data_forget($docker_compose, 'services.' . $this->container_name); data_forget($custom_compose, 'ip6');
$custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options); if ($ipv4 || $ipv6) {
if (count($custom_compose) > 0) { data_forget($docker_compose['services'][$this->container_name], 'networks');
$docker_compose['services'][$this->application->uuid] = array_merge_recursive($docker_compose['services'][$this->application->uuid], $custom_compose); }
if ($ipv4) {
$docker_compose['services'][$this->container_name]['networks'][$this->destination->network]['ipv4_address'] = $ipv4;
}
if ($ipv6) {
$docker_compose['services'][$this->container_name]['networks'][$this->destination->network]['ipv6_address'] = $ipv6;
}
$docker_compose['services'][$this->container_name] = array_merge_recursive($docker_compose['services'][$this->container_name], $custom_compose);
}
} else {
$docker_compose['services'][$this->application->uuid] = $docker_compose['services'][$this->container_name];
data_forget($docker_compose, 'services.' . $this->container_name);
$custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options);
if (count($custom_compose) > 0) {
$ipv4 = data_get($custom_compose, 'ip.0');
$ipv6 = data_get($custom_compose, 'ip6.0');
data_forget($custom_compose, 'ip');
data_forget($custom_compose, 'ip6');
if ($ipv4 || $ipv6) {
data_forget($docker_compose['services'][$this->application->uuid], 'networks');
}
if ($ipv4) {
$docker_compose['services'][$this->application->uuid]['networks'][$this->destination->network]['ipv4_address'] = $ipv4;
}
if ($ipv6) {
$docker_compose['services'][$this->application->uuid]['networks'][$this->destination->network]['ipv6_address'] = $ipv6;
}
$docker_compose['services'][$this->application->uuid] = array_merge_recursive($docker_compose['services'][$this->application->uuid], $custom_compose);
}
} }
} }
@@ -1507,18 +1530,18 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id); $containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id);
if ($this->pull_request_id === 0) { if ($this->pull_request_id === 0) {
$containers = $containers->filter(function ($container) { $containers = $containers->filter(function ($container) {
return data_get($container, 'Names') !== $this->container_name; return data_get($container, 'Names') !== $this->container_name && data_get($container, 'Names') !== $this->container_name . '-pr-' . $this->pull_request_id;
}); });
} }
$containers->each(function ($container) { $containers->each(function ($container) {
$containerName = data_get($container, 'Names'); $containerName = data_get($container, 'Names');
$this->execute_remote_command( $this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "docker rm -f $containerName >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true], ["docker rm -f $containerName >/dev/null 2>&1", "hidden" => true, "ignore_errors" => true],
); );
}); });
if ($this->application->settings->is_consistent_container_name_enabled) { if ($this->application->settings->is_consistent_container_name_enabled) {
$this->execute_remote_command( $this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true], ["docker rm -f $this->container_name >/dev/null 2>&1", "hidden" => true, "ignore_errors" => true],
); );
} }
} else { } else {
@@ -1527,7 +1550,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
'status' => ApplicationDeploymentStatus::FAILED->value, 'status' => ApplicationDeploymentStatus::FAILED->value,
]); ]);
$this->execute_remote_command( $this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true], ["docker rm -f $this->container_name >/dev/null 2>&1", "hidden" => true, "ignore_errors" => true],
); );
} }
} }
@@ -1634,6 +1657,8 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
public function failed(Throwable $exception): void public function failed(Throwable $exception): void
{ {
$this->next(ApplicationDeploymentStatus::FAILED->value);
$this->application_deployment_queue->addLogEntry("Oops something is not okay, are you okay? 😢", 'stderr'); $this->application_deployment_queue->addLogEntry("Oops something is not okay, are you okay? 😢", 'stderr');
if (str($exception->getMessage())->isNotEmpty()) { if (str($exception->getMessage())->isNotEmpty()) {
$this->application_deployment_queue->addLogEntry($exception->getMessage(), 'stderr'); $this->application_deployment_queue->addLogEntry($exception->getMessage(), 'stderr');
@@ -1641,15 +1666,14 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
if ($this->application->build_pack !== 'dockercompose') { if ($this->application->build_pack !== 'dockercompose') {
$code = $exception->getCode(); $code = $exception->getCode();
ray($code);
if ($code !== 69420) { if ($code !== 69420) {
// 69420 means failed to push the image to the registry, so we don't need to remove the new version as it is the currently running one // 69420 means failed to push the image to the registry, so we don't need to remove the new version as it is the currently running one
$this->application_deployment_queue->addLogEntry("Deployment failed. Removing the new version of your application.", 'stderr'); $this->application_deployment_queue->addLogEntry("Deployment failed. Removing the new version of your application.", 'stderr');
$this->execute_remote_command( $this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true] ["docker rm -f $this->container_name >/dev/null 2>&1", "hidden" => true, "ignore_errors" => true]
); );
} }
} }
$this->next(ApplicationDeploymentStatus::FAILED->value);
} }
} }

View File

@@ -30,6 +30,9 @@ class ApplicationPullRequestUpdateJob implements ShouldQueue, ShouldBeEncrypted
public function handle() public function handle()
{ {
try { try {
if ($this->application->is_public_repository()) {
return;
}
if ($this->status === ProcessStatus::CLOSED) { if ($this->status === ProcessStatus::CLOSED) {
$this->delete_comment(); $this->delete_comment();
return; return;

View File

@@ -43,6 +43,10 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
public function handle() public function handle()
{ {
if (!$this->server->isFunctional()) {
return 'Server is not ready.';
};
$applications = $this->server->applications(); $applications = $this->server->applications();
$skip_these_applications = collect([]); $skip_these_applications = collect([]);
foreach ($applications as $application) { foreach ($applications as $application) {
@@ -57,10 +61,6 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
$applications = $applications->filter(function ($value, $key) use ($skip_these_applications) { $applications = $applications->filter(function ($value, $key) use ($skip_these_applications) {
return !$skip_these_applications->pluck('id')->contains($value->id); return !$skip_these_applications->pluck('id')->contains($value->id);
}); });
if (!$this->server->isFunctional()) {
return 'Server is not ready.';
};
try { try {
if ($this->server->isSwarm()) { if ($this->server->isSwarm()) {
$containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this->server, false); $containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this->server, false);

View File

@@ -0,0 +1,74 @@
<?php
namespace App\Jobs;
use App\Models\ScheduledDatabaseBackup;
use App\Models\Server;
use App\Models\Team;
use App\Notifications\Database\DailyBackup;
use App\Notifications\Server\HighDiskUsage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class DatabaseBackupStatusJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 1;
public function __construct()
{
}
public function handle()
{
// $teams = Team::all();
// foreach ($teams as $team) {
// $scheduled_backups = $team->scheduledDatabaseBackups()->get();
// if ($scheduled_backups->isEmpty()) {
// continue;
// }
// foreach ($scheduled_backups as $scheduled_backup) {
// $last_days_backups = $scheduled_backup->get_last_days_backup_status();
// if ($last_days_backups->isEmpty()) {
// continue;
// }
// $failed = $last_days_backups->where('status', 'failed');
// }
// }
// $scheduled_backups = ScheduledDatabaseBackup::all();
// $databases = collect();
// $teams = collect();
// foreach ($scheduled_backups as $scheduled_backup) {
// $last_days_backups = $scheduled_backup->get_last_days_backup_status();
// if ($last_days_backups->isEmpty()) {
// continue;
// }
// $failed = $last_days_backups->where('status', 'failed');
// $database = $scheduled_backup->database;
// $team = $database->team();
// $teams->put($team->id, $team);
// $databases->put("{$team->id}:{$database->name}", [
// 'failed_count' => $failed->count(),
// ]);
// }
// foreach ($databases as $name => $database) {
// [$team_id, $name] = explode(':', $name);
// $team = $teams->get($team_id);
// $team?->notify(new DailyBackup($databases));
// }
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace App\Jobs;
use App\Models\Team;
use App\Notifications\Server\ForceDisabled;
use App\Notifications\Server\ForceEnabled;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class ServerLimitCheckJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 4;
public function backoff(): int
{
return isDev() ? 1 : 3;
}
public function __construct(public Team $team)
{
}
public function middleware(): array
{
return [(new WithoutOverlapping($this->team->uuid))];
}
public function uniqueId(): int
{
return $this->team->uuid;
}
public function handle()
{
try {
$servers = $this->team->servers;
$servers_count = $servers->count();
$limit = $this->team->limits['serverLimit'];
$number_of_servers_to_disable = $servers_count - $limit;
ray('ServerLimitCheckJob', $this->team->uuid, $servers_count, $limit, $number_of_servers_to_disable);
if ($number_of_servers_to_disable > 0) {
ray('Disabling servers');
$servers = $servers->sortbyDesc('created_at');
$servers_to_disable = $servers->take($number_of_servers_to_disable);
$servers_to_disable->each(function ($server) {
$server->forceDisableServer();
$this->team->notify(new ForceDisabled($server));
});
} else if ($number_of_servers_to_disable === 0) {
$servers->each(function ($server) {
if ($server->isForceDisabled()) {
$server->forceEnableServer();
$this->team->notify(new ForceEnabled($server));
}
});
}
} catch (\Throwable $e) {
send_internal_notification('ServerLimitCheckJob failed with: ' . $e->getMessage());
ray($e->getMessage());
return handleError($e);
}
}
}

View File

@@ -41,15 +41,6 @@ class ServerStatusJob implements ShouldQueue, ShouldBeEncrypted
throw new \RuntimeException('Server is not ready.'); throw new \RuntimeException('Server is not ready.');
}; };
try { try {
// $this->server->validateConnection();
// $this->server->validateOS();
// $docker_installed = $this->server->validateDockerEngine();
// if (!$docker_installed) {
// $this->server->installDocker();
// $this->server->validateDockerEngine();
// }
// $this->server->validateDockerEngineVersion();
if ($this->server->isFunctional()) { if ($this->server->isFunctional()) {
$this->cleanup(notify: false); $this->cleanup(notify: false);
} }

View File

@@ -0,0 +1,52 @@
<?php
namespace App\Listeners;
use Illuminate\Foundation\Events\MaintenanceModeDisabled as EventsMaintenanceModeDisabled;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
class MaintenanceModeDisabledNotification
{
public function __construct()
{
}
public function handle(EventsMaintenanceModeDisabled $event): void
{
ray('Maintenance mode disabled!');
$files = Storage::disk('webhooks-during-maintenance')->files();
$files = collect($files);
$files = $files->sort();
foreach ($files as $file) {
$content = Storage::disk('webhooks-during-maintenance')->get($file);
$data = json_decode($content, true);
$symfonyRequest = new SymfonyRequest(
$data['query'],
$data['request'],
$data['attributes'],
$data['cookies'],
$data['files'],
$data['server'],
$data['content']
);
foreach ($data['headers'] as $key => $value) {
$symfonyRequest->headers->set($key, $value);
}
$request = Request::createFromBase($symfonyRequest);
$endpoint = str($file)->after('_')->beforeLast('_')->value();
$class = "App\Http\Controllers\Webhook\\" . ucfirst(str($endpoint)->before('::')->value());
$method = str($endpoint)->after('::')->value();
try {
$instance = new $class();
$instance->$method($request);
} catch (\Throwable $th) {
ray($th);
} finally {
Storage::disk('webhooks-during-maintenance')->delete($file);
}
}
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Listeners;
use App\Events\MaintenanceModeEnabled;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Events\MaintenanceModeEnabled as EventsMaintenanceModeEnabled;
use Illuminate\Queue\InteractsWithQueue;
class MaintenanceModeEnabledNotification
{
/**
* Create the event listener.
*/
public function __construct()
{
//
}
/**
* Handle the event.
*/
public function handle(EventsMaintenanceModeEnabled $event): void
{
ray('Maintenance mode enabled!');
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Livewire\Admin; namespace App\Livewire\Admin;
use App\Models\User; use App\Models\User;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Crypt;
use Livewire\Component; use Livewire\Component;
@@ -14,28 +15,26 @@ class Index extends Component
if (!isCloud()) { if (!isCloud()) {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
if (auth()->user()->id !== 0 && session('adminToken') === null) { if (auth()->user()->id !== 0) {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
$this->users = User::whereHas('teams', function ($query) { $this->users = User::whereHas('teams', function ($query) {
$query->whereRelation('subscription', 'stripe_subscription_id', '!=', null); $query->whereRelation('subscription', 'stripe_subscription_id', '!=', null);
})->get(); })->get()->filter(function ($user) {
return $user->id !== 0;
});
} }
public function switchUser(int $user_id) public function switchUser(int $user_id)
{ {
$user = User::find($user_id); if (auth()->user()->id !== 0) {
auth()->login($user); return redirect()->route('dashboard');
if ($user_id === 0) {
session()->forget('adminToken');
} else {
$token_payload = [
'valid' => true,
];
$token = Crypt::encrypt($token_payload);
session(['adminToken' => $token]);
} }
return refreshSession(); $user = User::find($user_id);
$team_to_switch_to = $user->teams->first();
Cache::forget("team:{$user->id}");
auth()->login($user);
refreshSession($team_to_switch_to);
return redirect(request()->header('Referer'));
} }
public function render() public function render()
{ {

View File

@@ -23,8 +23,8 @@ class Dashboard extends Component
public function cleanup_queue() public function cleanup_queue()
{ {
$this->dispatch('success', 'Cleanup started.'); $this->dispatch('success', 'Cleanup started.');
Artisan::queue('app:init', [ Artisan::queue('cleanup:application-deployment-queue', [
'--cleanup-deployments' => 'true' '--team-id' => currentTeam()->id
]); ]);
} }
public function get_deployments() public function get_deployments()

View File

@@ -4,7 +4,7 @@ namespace App\Livewire;
use Livewire\Component; use Livewire\Component;
class Sponsorship extends Component class LayoutPopups extends Component
{ {
public function getListeners() public function getListeners()
{ {
@@ -23,6 +23,6 @@ class Sponsorship extends Component
} }
public function render() public function render()
{ {
return view('livewire.sponsorship'); return view('livewire.layout-popups');
} }
} }

View File

@@ -10,7 +10,7 @@ class Configuration extends Component
{ {
public Application $application; public Application $application;
public $servers; public $servers;
protected $listeners = ['build_pack_updated' => '$refresh']; protected $listeners = ['buildPackUpdated' => '$refresh'];
public function mount() public function mount()
{ {

View File

@@ -182,7 +182,7 @@ class General extends Component
$this->resetDefaultLabels(false); $this->resetDefaultLabels(false);
} }
$this->submit(); $this->submit();
$this->dispatch('build_pack_updated'); $this->dispatch('buildPackUpdated');
} }
public function getWildcardDomain() public function getWildcardDomain()
{ {

View File

@@ -29,8 +29,8 @@ class Import extends Component
public string $container; public string $container;
public array $importCommands = []; public array $importCommands = [];
public string $postgresqlRestoreCommand = 'pg_restore -U $POSTGRES_USER -d $POSTGRES_DB'; public string $postgresqlRestoreCommand = 'pg_restore -U $POSTGRES_USER -d $POSTGRES_DB';
public string $mysqlRestoreCommand = 'mysql -u $MYSQL_USER -p $MYSQL_PASSWORD $MYSQL_DATABASE'; public string $mysqlRestoreCommand = 'mysql -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE';
public string $mariadbRestoreCommand = 'mariadb -u $MARIADB_USER -p $MARIADB_PASSWORD $MARIADB_DATABASE'; public string $mariadbRestoreCommand = 'mariadb -u $MARIADB_USER -p$MARIADB_PASSWORD $MARIADB_DATABASE';
public function getListeners() public function getListeners()
{ {

View File

@@ -10,7 +10,8 @@ use Livewire\Component;
class Create extends Component class Create extends Component
{ {
public $type; public $type;
public function mount() { public function mount()
{
$services = getServiceTemplates(); $services = getServiceTemplates();
$type = str(request()->query('type')); $type = str(request()->query('type'));
$destination_uuid = request()->query('destination'); $destination_uuid = request()->query('destination');
@@ -70,7 +71,7 @@ class Create extends Component
$generatedValue = $value; $generatedValue = $value;
if ($value->contains('SERVICE_')) { if ($value->contains('SERVICE_')) {
$command = $value->after('SERVICE_')->beforeLast('_'); $command = $value->after('SERVICE_')->beforeLast('_');
$generatedValue = generateEnvValue($command->value()); $generatedValue = generateEnvValue($command->value(), $service);
} }
EnvironmentVariable::create([ EnvironmentVariable::create([
'key' => $key, 'key' => $key,

View File

@@ -17,9 +17,7 @@ class Configuration extends Component
{ {
$userId = auth()->user()->id; $userId = auth()->user()->id;
return [ return [
"echo-private:user.{$userId},ServiceStatusChanged" => 'checkStatus', "echo-private:user.{$userId},ServiceStatusChanged" => 'check_status',
"refreshStacks",
"checkStatus",
]; ];
} }
public function render() public function render()
@@ -37,21 +35,10 @@ class Configuration extends Component
$this->applications = $this->service->applications->sort(); $this->applications = $this->service->applications->sort();
$this->databases = $this->service->databases->sort(); $this->databases = $this->service->databases->sort();
} }
public function checkStatus() public function check_status()
{ {
dispatch_sync(new ContainerStatusJob($this->service->server)); dispatch_sync(new ContainerStatusJob($this->service->server));
$this->refreshStacks(); $this->dispatch('refresh')->self();
$this->dispatch('serviceStatusChanged'); $this->dispatch('serviceStatusChanged');
} }
public function refreshStacks()
{
$this->applications = $this->service->applications->sort();
$this->applications->each(function ($application) {
$application->refresh();
});
$this->databases = $this->service->databases->sort();
$this->databases->each(function ($database) {
$database->refresh();
});
}
} }

View File

@@ -17,7 +17,20 @@ class Navbar extends Component
public array $parameters; public array $parameters;
public array $query; public array $query;
public $isDeploymentProgress = false; public $isDeploymentProgress = false;
public function getListeners()
{
return [
"serviceStatusChanged"
];
}
public function serviceStatusChanged()
{
$this->dispatch('refresh')->self();
}
public function render()
{
return view('livewire.project.service.navbar');
}
public function checkDeployments() public function checkDeployments()
{ {
$activity = Activity::where('properties->type_uuid', $this->service->uuid)->latest()->first(); $activity = Activity::where('properties->type_uuid', $this->service->uuid)->latest()->first();
@@ -28,26 +41,6 @@ class Navbar extends Component
$this->isDeploymentProgress = false; $this->isDeploymentProgress = false;
} }
} }
public function getListeners()
{
return [
"serviceStatusChanged"
];
}
public function serviceStatusChanged()
{
$this->service->refresh();
}
public function render()
{
return view('livewire.project.service.navbar');
}
public function check_status($showNotification = false)
{
dispatch_sync(new ContainerStatusJob($this->service->destination->server));
$this->service->refresh();
if ($showNotification) $this->dispatch('success', 'Service status updated.');
}
public function deploy() public function deploy()
{ {
$this->checkDeployments(); $this->checkDeployments();
@@ -62,9 +55,8 @@ class Navbar extends Component
public function stop(bool $forceCleanup = false) public function stop(bool $forceCleanup = false)
{ {
StopService::run($this->service); StopService::run($this->service);
$this->service->refresh();
if ($forceCleanup) { if ($forceCleanup) {
$this->dispatch('success', 'Force cleanup service.'); $this->dispatch('success', 'Containers cleaned up.');
} else { } else {
$this->dispatch('success', 'Service stopped.'); $this->dispatch('success', 'Service stopped.');
} }

View File

@@ -10,29 +10,21 @@ use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql; use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis; use App\Models\StandaloneRedis;
use Illuminate\Support\Collection;
use Livewire\Component; use Livewire\Component;
class ExecuteContainerCommand extends Component class ExecuteContainerCommand extends Component
{ {
public string $command; public string $command;
public string $container; public string $container;
public $containers; public Collection $containers;
public $parameters; public $parameters;
public $resource; public $resource;
public string $type; public string $type;
public string $workDir = ''; public string $workDir = '';
public Server $server; public Server $server;
public $servers = []; public Collection $servers;
public function getListeners()
{
return [
"serviceStatusChanged",
];
}
public function serviceStatusChanged()
{
$this->getContainers();
}
protected $rules = [ protected $rules = [
'server' => 'required', 'server' => 'required',
'container' => 'required', 'container' => 'required',
@@ -43,20 +35,18 @@ class ExecuteContainerCommand extends Component
public function mount() public function mount()
{ {
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
$this->getContainers();
}
public function getContainers()
{
$this->containers = collect(); $this->containers = collect();
$this->servers = collect();
if (data_get($this->parameters, 'application_uuid')) { if (data_get($this->parameters, 'application_uuid')) {
$this->type = 'application'; $this->type = 'application';
$this->resource = Application::where('uuid', $this->parameters['application_uuid'])->firstOrFail(); $this->resource = Application::where('uuid', $this->parameters['application_uuid'])->firstOrFail();
$this->server = $this->resource->destination->server; if ($this->resource->destination->server->isFunctional()) {
$containers = getCurrentApplicationContainerStatus($this->server, $this->resource->id, 0); $this->servers = $this->servers->push($this->resource->destination->server);
if ($containers->count() > 0) { }
$containers->each(function ($container) { foreach ($this->resource->additional_servers as $server) {
$this->containers->push(str_replace('/', '', $container['Names'])); if ($server->isFunctional()) {
}); $this->servers = $this->servers->push($server);
}
} }
} else if (data_get($this->parameters, 'database_uuid')) { } else if (data_get($this->parameters, 'database_uuid')) {
$this->type = 'database'; $this->type = 'database';
@@ -77,44 +67,85 @@ class ExecuteContainerCommand extends Component
} }
} }
$this->resource = $resource; $this->resource = $resource;
$this->server = $this->resource->destination->server; if ($this->resource->destination->server->isFunctional()) {
$this->servers = $this->servers->push($this->resource->destination->server);
}
$this->container = $this->resource->uuid; $this->container = $this->resource->uuid;
// if (!str(data_get($this,'resource.status'))->startsWith('exited')) { $this->containers->push($this->container);
$this->containers->push($this->container);
// }
} else if (data_get($this->parameters, 'service_uuid')) { } else if (data_get($this->parameters, 'service_uuid')) {
$this->type = 'service'; $this->type = 'service';
$this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail(); $this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail();
$this->resource->applications()->get()->each(function ($application) { $this->resource->applications()->get()->each(function ($application) {
// if (str(data_get($application, 'status'))->contains('running')) { $this->containers->push(data_get($application, 'name') . '-' . data_get($this->resource, 'uuid'));
$this->containers->push(data_get($application, 'name') . '-' . data_get($this->resource, 'uuid'));
// }
}); });
$this->resource->databases()->get()->each(function ($database) { $this->resource->databases()->get()->each(function ($database) {
// if (str(data_get($database, 'status'))->contains('running')) { $this->containers->push(data_get($database, 'name') . '-' . data_get($this->resource, 'uuid'));
$this->containers->push(data_get($database, 'name') . '-' . data_get($this->resource, 'uuid'));
// }
}); });
if ($this->resource->server->isFunctional()) {
$this->server = $this->resource->server; $this->servers = $this->servers->push($this->resource->server);
}
} }
if ($this->containers->count() > 0) { if ($this->containers->count() > 0) {
$this->container = $this->containers->first(); $this->container = $this->containers->first();
} }
} }
public function loadContainers()
{
foreach ($this->servers as $server) {
if (data_get($this->parameters, 'application_uuid')) {
if ($server->isSwarm()) {
$containers = collect([
[
'Names' => $this->resource->uuid . '_' . $this->resource->uuid,
]
]);
} else {
$containers = getCurrentApplicationContainerStatus($server, $this->resource->id, includePullrequests: true);
}
foreach ($containers as $container) {
$payload = [
'server' => $server,
'container' => $container,
];
$this->containers = $this->containers->push($payload);
}
}
}
if ($this->containers->count() > 0) {
if (data_get($this->parameters, 'application_uuid')) {
$this->container = data_get($this->containers->first(), 'container.Names');
} elseif (data_get($this->parameters, 'database_uuid')) {
$this->container = $this->containers->first();
} elseif (data_get($this->parameters, 'service_uuid')) {
$this->container = $this->containers->first();
}
}
}
public function runCommand() public function runCommand()
{ {
$this->validate();
try { try {
// Wrap command to prevent escaped execution in the host. if (data_get($this->parameters, 'application_uuid')) {
$container = $this->containers->where('container.Names', $this->container)->first();
$container_name = data_get($container, 'container.Names');
if (is_null($container)) {
throw new \RuntimeException('Container not found.');
}
$server = data_get($container, 'server');
} else {
$container_name = $this->container;
$server = $this->servers->first();
}
if ($server->isForceDisabled()) {
throw new \RuntimeException('Server is disabled.');
}
$cmd = 'sh -c "if [ -f ~/.profile ]; then . ~/.profile; fi; ' . str_replace('"', '\"', $this->command) . '"'; $cmd = 'sh -c "if [ -f ~/.profile ]; then . ~/.profile; fi; ' . str_replace('"', '\"', $this->command) . '"';
if (!empty($this->workDir)) { if (!empty($this->workDir)) {
$exec = "docker exec -w {$this->workDir} {$this->container} {$cmd}"; $exec = "docker exec -w {$this->workDir} {$container_name} {$cmd}";
} else { } else {
$exec = "docker exec {$this->container} {$cmd}"; $exec = "docker exec {$container_name} {$cmd}";
} }
$activity = remote_process([$exec], $this->server, ignore_errors: true); $activity = remote_process([$exec], $server, ignore_errors: true);
$this->dispatch('activityMonitor', $activity->id); $this->dispatch('activityMonitor', $activity->id);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);

View File

@@ -23,6 +23,7 @@ class GetLogs extends Component
public ServiceApplication|ServiceDatabase|null $servicesubtype = null; public ServiceApplication|ServiceDatabase|null $servicesubtype = null;
public Server $server; public Server $server;
public ?string $container = null; public ?string $container = null;
public ?string $pull_request = null;
public ?bool $streamLogs = false; public ?bool $streamLogs = false;
public ?bool $showTimeStamps = true; public ?bool $showTimeStamps = true;
public int $numberOfLines = 100; public int $numberOfLines = 100;
@@ -70,7 +71,14 @@ class GetLogs extends Component
} }
public function getLogs($refresh = false) public function getLogs($refresh = false)
{ {
if (!$refresh && $this->resource?->getMorphClass() === 'App\Models\Service') return; if ($this->resource?->getMorphClass() === 'App\Models\Application') {
if (str($this->container)->contains('-pr-')) {
$this->pull_request = "Pull Request: " . str($this->container)->afterLast('-pr-')->beforeLast('_')->value();
} else {
$this->pull_request = 'branch';
}
}
if (!$refresh && ($this->resource?->getMorphClass() === 'App\Models\Service' || str($this->container)->contains('-pr-'))) return;
if ($this->container) { if ($this->container) {
if ($this->showTimeStamps) { if ($this->showTimeStamps) {
if ($this->server->isSwarm()) { if ($this->server->isSwarm()) {

View File

@@ -10,43 +10,56 @@ use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql; use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis; use App\Models\StandaloneRedis;
use Illuminate\Support\Collection;
use Livewire\Component; use Livewire\Component;
class Logs extends Component class Logs extends Component
{ {
public ?string $type = null; public ?string $type = null;
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $resource; public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $resource;
public Server $server; public Collection $servers;
public Collection $containers;
public $container = []; public $container = [];
public $containers;
public $parameters; public $parameters;
public $query; public $query;
public $status; public $status;
public $serviceSubType; public $serviceSubType;
public function mount() public function loadContainers($server_id)
{ {
$this->containers = collect(); try {
$this->parameters = get_route_parameters(); $server = $this->servers->firstWhere('id', $server_id);
$this->query = request()->query(); if ($server->isSwarm()) {
if (data_get($this->parameters, 'application_uuid')) {
$this->type = 'application';
$this->resource = Application::where('uuid', $this->parameters['application_uuid'])->firstOrFail();
$this->status = $this->resource->status;
$this->server = $this->resource->destination->server;
if ($this->server->isSwarm()) {
$containers = collect([ $containers = collect([
[ [
'Names' => $this->resource->uuid . '_' . $this->resource->uuid, 'Names' => $this->resource->uuid . '_' . $this->resource->uuid,
] ]
]); ]);
} else { } else {
$containers = getCurrentApplicationContainerStatus($this->server, $this->resource->id, 0); $containers = getCurrentApplicationContainerStatus($server, $this->resource->id, includePullrequests: true);
} }
if ($containers->count() > 0) { $server->containers = $containers;
$containers->each(function ($container) { } catch (\Exception $e) {
$this->containers->push(str_replace('/', '', $container['Names'])); return handleError($e, $this);
}); }
}
public function mount()
{
$this->containers = collect();
$this->servers = collect();
$this->parameters = get_route_parameters();
$this->query = request()->query();
if (data_get($this->parameters, 'application_uuid')) {
$this->type = 'application';
$this->resource = Application::where('uuid', $this->parameters['application_uuid'])->firstOrFail();
$this->status = $this->resource->status;
if ($this->resource->destination->server->isFunctional()) {
$this->servers = $this->servers->push($this->resource->destination->server);
}
foreach ($this->resource->additional_servers as $server) {
if ($server->isFunctional()) {
$this->servers = $this->servers->push($server);
}
} }
} else if (data_get($this->parameters, 'database_uuid')) { } else if (data_get($this->parameters, 'database_uuid')) {
$this->type = 'database'; $this->type = 'database';
@@ -68,26 +81,24 @@ class Logs extends Component
} }
$this->resource = $resource; $this->resource = $resource;
$this->status = $this->resource->status; $this->status = $this->resource->status;
$this->server = $this->resource->destination->server; if ($this->resource->destination->server->isFunctional()) {
$this->servers = $this->servers->push($this->resource->destination->server);
}
$this->container = $this->resource->uuid; $this->container = $this->resource->uuid;
// if (str(data_get($this, 'resource.status'))->startsWith('running')) { $this->containers->push($this->container);
$this->containers->push($this->container);
// }
} else if (data_get($this->parameters, 'service_uuid')) { } else if (data_get($this->parameters, 'service_uuid')) {
$this->type = 'service'; $this->type = 'service';
$this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail(); $this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail();
$this->resource->applications()->get()->each(function ($application) { $this->resource->applications()->get()->each(function ($application) {
// if (str(data_get($application, 'status'))->contains('running')) { $this->containers->push(data_get($application, 'name') . '-' . data_get($this->resource, 'uuid'));
$this->containers->push(data_get($application, 'name') . '-' . data_get($this->resource, 'uuid'));
// }
}); });
$this->resource->databases()->get()->each(function ($database) { $this->resource->databases()->get()->each(function ($database) {
// if (str(data_get($database, 'status'))->contains('running')) { $this->containers->push(data_get($database, 'name') . '-' . data_get($this->resource, 'uuid'));
$this->containers->push(data_get($database, 'name') . '-' . data_get($this->resource, 'uuid'));
// }
}); });
$this->server = $this->resource->server; if ($this->resource->server->isFunctional()) {
$this->servers = $this->servers->push($this->resource->server);
}
} }
} }

View File

@@ -3,6 +3,7 @@
namespace App\Livewire\Server; namespace App\Livewire\Server;
use App\Models\PrivateKey; use App\Models\PrivateKey;
use App\Models\Team;
use Livewire\Component; use Livewire\Component;
class Create extends Component class Create extends Component
@@ -16,11 +17,7 @@ class Create extends Component
$this->limit_reached = false; $this->limit_reached = false;
return; return;
} }
$team = currentTeam(); $this->limit_reached = Team::serverLimitReached();
$servers = $team->servers->count();
['serverLimit' => $serverLimit] = $team->limits;
$this->limit_reached = $servers >= $serverLimit;
} }
public function render() public function render()
{ {

View File

@@ -5,6 +5,7 @@ namespace App\Livewire\Server\New;
use App\Enums\ProxyStatus; use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes; use App\Enums\ProxyTypes;
use App\Models\Server; use App\Models\Server;
use App\Models\Team;
use Livewire\Component; use Livewire\Component;
class ByIp extends Component class ByIp extends Component
@@ -76,6 +77,9 @@ class ByIp extends Component
if (is_null($this->private_key_id)) { if (is_null($this->private_key_id)) {
return $this->dispatch('error', 'You must select a private key'); return $this->dispatch('error', 'You must select a private key');
} }
if (Team::serverLimitReached()) {
return $this->dispatch('error', 'You have reached the server limit for your subscription.');
}
$payload = [ $payload = [
'name' => $this->name, 'name' => $this->name,
'description' => $this->description, 'description' => $this->description,

View File

@@ -55,7 +55,6 @@ class Resources extends Component
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }
$this->loadUnmanagedContainers();
} }
public function render() public function render()
{ {

View File

@@ -2,17 +2,17 @@
namespace App\Livewire\Subscription; namespace App\Livewire\Subscription;
use App\Models\Team;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Livewire\Component; use Livewire\Component;
class Actions extends Component class Actions extends Component
{ {
public $server_limits = 0; public $server_limits = 0;
public function mount() public function mount()
{ {
$limits = currentTeam()->limits; $this->server_limits = Team::serverLimit();
$this->server_limits = data_get($limits, 'serverLimit', 0);
} }
public function cancel() public function cancel()
{ {

View File

@@ -9,8 +9,9 @@ use Stripe\Checkout\Session;
class PricingPlans extends Component class PricingPlans extends Component
{ {
public bool $isTrial = false; public bool $isTrial = false;
public function mount() { public function mount()
$this->isTrial = !data_get(currentTeam(),'subscription.stripe_trial_already_ended'); {
$this->isTrial = !data_get(currentTeam(), 'subscription.stripe_trial_already_ended');
if (config('constants.limits.trial_period') == 0) { if (config('constants.limits.trial_period') == 0) {
$this->isTrial = false; $this->isTrial = false;
} }
@@ -26,15 +27,15 @@ class PricingPlans extends Component
case 'basic-yearly': case 'basic-yearly':
$priceId = config('subscription.stripe_price_id_basic_yearly'); $priceId = config('subscription.stripe_price_id_basic_yearly');
break; break;
case 'ultimate-monthly':
$priceId = config('subscription.stripe_price_id_ultimate_monthly');
break;
case 'pro-monthly': case 'pro-monthly':
$priceId = config('subscription.stripe_price_id_pro_monthly'); $priceId = config('subscription.stripe_price_id_pro_monthly');
break; break;
case 'pro-yearly': case 'pro-yearly':
$priceId = config('subscription.stripe_price_id_pro_yearly'); $priceId = config('subscription.stripe_price_id_pro_yearly');
break; break;
case 'ultimate-monthly':
$priceId = config('subscription.stripe_price_id_ultimate_monthly');
break;
case 'ultimate-yearly': case 'ultimate-yearly':
$priceId = config('subscription.stripe_price_id_ultimate_yearly'); $priceId = config('subscription.stripe_price_id_ultimate_yearly');
break; break;
@@ -64,18 +65,25 @@ class PricingPlans extends Component
'success_url' => route('dashboard', ['success' => true]), 'success_url' => route('dashboard', ['success' => true]),
'cancel_url' => route('subscription.index', ['cancelled' => true]), 'cancel_url' => route('subscription.index', ['cancelled' => true]),
]; ];
if (str($type)->contains('ultimate')) {
if (!data_get($team,'subscription.stripe_trial_already_ended')) { $payload['line_items'][0]['adjustable_quantity'] = [
if (config('constants.limits.trial_period') > 0) { 'enabled' => true,
$payload['subscription_data'] = [ 'minimum' => 10,
'trial_period_days' => config('constants.limits.trial_period'),
'trial_settings' => [
'end_behavior' => [
'missing_payment_method' => 'cancel',
]
],
]; ];
$payload['line_items'][0]['quantity'] = 10;
} }
if (!data_get($team, 'subscription.stripe_trial_already_ended')) {
if (config('constants.limits.trial_period') > 0) {
$payload['subscription_data'] = [
'trial_period_days' => config('constants.limits.trial_period'),
'trial_settings' => [
'end_behavior' => [
'missing_payment_method' => 'cancel',
]
],
];
}
$payload['payment_method_collection'] = 'if_required'; $payload['payment_method_collection'] = 'if_required';
} }
$customer = currentTeam()->subscription?->stripe_customer_id ?? null; $customer = currentTeam()->subscription?->stripe_customer_id ?? null;

View File

@@ -65,6 +65,13 @@ class Application extends BaseModel
return $this->belongsToMany(StandaloneDocker::class, 'additional_destinations') return $this->belongsToMany(StandaloneDocker::class, 'additional_destinations')
->withPivot('server_id', 'status'); ->withPivot('server_id', 'status');
} }
public function is_public_repository(): bool
{
if (data_get($this, 'source.is_public')) {
return true;
}
return false;
}
public function is_github_based(): bool public function is_github_based(): bool
{ {
if (data_get($this, 'source')) { if (data_get($this, 'source')) {
@@ -395,7 +402,10 @@ class Application extends BaseModel
} }
return false; return false;
} }
public function get_last_days_deployments()
{
return ApplicationDeploymentQueue::where('application_id', $this->id)->where('created_at', '>=', now()->subDays(7))->orderBy('created_at', 'desc')->get();
}
public function deployments(int $skip = 0, int $take = 10) public function deployments(int $skip = 0, int $take = 10)
{ {
$deployments = ApplicationDeploymentQueue::where('application_id', $this->id)->orderBy('created_at', 'desc'); $deployments = ApplicationDeploymentQueue::where('application_id', $this->id)->orderBy('created_at', 'desc');

View File

@@ -30,4 +30,8 @@ class ScheduledDatabaseBackup extends BaseModel
{ {
return $this->belongsTo(S3Storage::class, 's3_storage_id'); return $this->belongsTo(S3Storage::class, 's3_storage_id');
} }
public function get_last_days_backup_status($days = 7)
{
return $this->hasMany(ScheduledDatabaseBackupExecution::class)->where('created_at', '>=', now()->subDays($days))->get();
}
} }

View File

@@ -10,6 +10,7 @@ use App\Notifications\Server\Unreachable;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Spatie\SchemalessAttributes\Casts\SchemalessAttributes; use Spatie\SchemalessAttributes\Casts\SchemalessAttributes;
use Spatie\SchemalessAttributes\SchemalessAttributesTrait; use Spatie\SchemalessAttributes\SchemalessAttributesTrait;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@@ -69,7 +70,7 @@ class Server extends BaseModel
static public function isUsable() static public function isUsable()
{ {
return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_swarm_worker', false)->whereRelation('settings', 'is_build_server', false); return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_swarm_worker', false)->whereRelation('settings', 'is_build_server', false)->whereRelation('settings', 'force_disabled', false);
} }
static public function destinationsByServer(string $server_id) static public function destinationsByServer(string $server_id)
@@ -146,11 +147,34 @@ class Server extends BaseModel
public function skipServer() public function skipServer()
{ {
if ($this->ip === '1.2.3.4') { if ($this->ip === '1.2.3.4') {
ray('skipping 1.2.3.4'); // ray('skipping 1.2.3.4');
return true;
}
if ($this->settings->force_disabled === true) {
// ray('force_disabled');
return true; return true;
} }
return false; return false;
} }
public function isForceDisabled()
{
return $this->settings->force_disabled;
}
public function forceEnableServer()
{
$this->settings->update([
'force_disabled' => false,
]);
}
public function forceDisableServer()
{
$this->settings->update([
'force_disabled' => true,
]);
$sshKeyFileLocation = "id.root@{$this->uuid}";
Storage::disk('ssh-keys')->delete($sshKeyFileLocation);
Storage::disk('ssh-mux')->delete($this->muxFilename());
}
public function isServerReady(int $tries = 3) public function isServerReady(int $tries = 3)
{ {
if ($this->skipServer()) { if ($this->skipServer()) {
@@ -239,17 +263,21 @@ class Server extends BaseModel
} }
public function loadUnmanagedContainers() public function loadUnmanagedContainers()
{ {
$containers = instant_remote_process(["docker ps -a --format '{{json .}}' "], $this); if ($this->isFunctional()) {
$containers = format_docker_command_output_to_json($containers); $containers = instant_remote_process(["docker ps -a --format '{{json .}}' "], $this);
$containers = $containers->map(function ($container) { $containers = format_docker_command_output_to_json($containers);
$labels = data_get($container, 'Labels'); $containers = $containers->map(function ($container) {
if (!str($labels)->contains("coolify.managed")) { $labels = data_get($container, 'Labels');
return $container; if (!str($labels)->contains("coolify.managed")) {
} return $container;
return null; }
}); return null;
$containers = $containers->filter(); });
return collect($containers); $containers = $containers->filter();
return collect($containers);
} else {
return collect([]);
}
} }
public function hasDefinedResources() public function hasDefinedResources()
{ {
@@ -374,7 +402,7 @@ class Server extends BaseModel
} }
public function isFunctional() public function isFunctional()
{ {
return $this->settings->is_reachable && $this->settings->is_usable; return $this->settings->is_reachable && $this->settings->is_usable && !$this->settings->force_disabled;
} }
public function isLogDrainEnabled() public function isLogDrainEnabled()
{ {

View File

@@ -102,6 +102,30 @@ class Service extends BaseModel
foreach ($applications as $application) { foreach ($applications as $application) {
$image = str($application->image)->before(':')->value(); $image = str($application->image)->before(':')->value();
switch ($image) { switch ($image) {
case str($image)?->contains('kong'):
$data = collect([]);
$dashboard_user = $this->environment_variables()->where('key', 'SERVICE_USER_ADMIN')->first();
$dashboard_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ADMIN')->first();
if ($dashboard_user) {
$data = $data->merge([
'Dashboard User' => [
'key' => data_get($dashboard_user, 'key'),
'value' => data_get($dashboard_user, 'value'),
'rules' => 'required',
],
]);
}
if ($dashboard_password) {
$data = $data->merge([
'Dashboard Password' => [
'key' => data_get($dashboard_password, 'key'),
'value' => data_get($dashboard_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
}
$fields->put('Supabase', $data->toArray());
case str($image)?->contains('minio'): case str($image)?->contains('minio'):
$data = collect([]); $data = collect([]);
$console_url = $this->environment_variables()->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first(); $console_url = $this->environment_variables()->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first();

View File

@@ -48,7 +48,25 @@ class Team extends Model implements SendsDiscord, SendsEmail
} }
return explode(',', $recipients); return explode(',', $recipients);
} }
static public function serverLimitReached() {
$serverLimit = Team::serverLimit();
$team = currentTeam();
$servers = $team->servers->count();
return $servers >= $serverLimit;
}
public function serverOverflow() {
if ($this->serverLimit() < $this->servers->count()) {
return true;
}
return false;
}
static public function serverLimit()
{
if (currentTeam()->id === 0 && isDev()) {
return 9999999;
}
return Team::find(currentTeam()->id)->limits['serverLimit'];
}
public function limits(): Attribute public function limits(): Attribute
{ {
return Attribute::make( return Attribute::make(
@@ -63,14 +81,19 @@ class Team extends Model implements SendsDiscord, SendsEmail
$subscription = $subscription->type(); $subscription = $subscription->type();
} }
} }
$serverLimit = config('constants.limits.server')[strtolower($subscription)]; if ($this->custom_server_limit) {
$serverLimit = $this->custom_server_limit;
} else {
$serverLimit = config('constants.limits.server')[strtolower($subscription)];
}
$sharedEmailEnabled = config('constants.limits.email')[strtolower($subscription)]; $sharedEmailEnabled = config('constants.limits.email')[strtolower($subscription)];
return ['serverLimit' => $serverLimit, 'sharedEmailEnabled' => $sharedEmailEnabled]; return ['serverLimit' => $serverLimit, 'sharedEmailEnabled' => $sharedEmailEnabled];
} }
); );
} }
public function environment_variables() { public function environment_variables()
{
return $this->hasMany(SharedEnvironmentVariable::class)->whereNull('project_id')->whereNull('environment_id'); return $this->hasMany(SharedEnvironmentVariable::class)->whereNull('project_id')->whereNull('environment_id');
} }
public function members() public function members()
@@ -130,7 +153,8 @@ class Team extends Model implements SendsDiscord, SendsEmail
{ {
return $this->hasMany(S3Storage::class)->where('is_usable', true); return $this->hasMany(S3Storage::class)->where('is_usable', true);
} }
public function trialEnded() { public function trialEnded()
{
foreach ($this->servers as $server) { foreach ($this->servers as $server) {
$server->settings()->update([ $server->settings()->update([
'is_usable' => false, 'is_usable' => false,
@@ -138,7 +162,8 @@ class Team extends Model implements SendsDiscord, SendsEmail
]); ]);
} }
} }
public function trialEndedButSubscribed() { public function trialEndedButSubscribed()
{
foreach ($this->servers as $server) { foreach ($this->servers as $server) {
$server->settings()->update([ $server->settings()->update([
'is_usable' => true, 'is_usable' => true,

View File

@@ -43,7 +43,13 @@ class DeploymentSuccess extends Notification implements ShouldQueue
public function via(object $notifiable): array public function via(object $notifiable): array
{ {
return setNotificationChannels($notifiable, 'deployments'); $channels = setNotificationChannels($notifiable, 'deployments');
if (isCloud()) {
$channels = array_filter($channels, function ($channel) {
return $channel !== 'App\Notifications\Channels\EmailChannel';
});
}
return $channels;
} }
public function toMail(): MailMessage public function toMail(): MailMessage
@@ -69,7 +75,7 @@ class DeploymentSuccess extends Notification implements ShouldQueue
public function toDiscord(): string public function toDiscord(): string
{ {
if ($this->preview) { if ($this->preview) {
$message = 'Coolify: New PR' . $this->preview->pull_request_id . ' version successfully deployed of ' . $this->application_name . ' $message = 'Coolify: New PR' . $this->preview->pull_request_id . ' version successfully deployed of ' . $this->application_name . '
'; ';
if ($this->preview->fqdn) { if ($this->preview->fqdn) {

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Notifications\Database;
use App\Models\ScheduledDatabaseBackup;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\TelegramChannel;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Channels\MailChannel;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class DailyBackup extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 1;
public function __construct(public $databases)
{
}
public function via(object $notifiable): array
{
return [DiscordChannel::class, TelegramChannel::class, MailChannel::class];
}
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject("Coolify: Daily backup statuses");
$mail->view('emails.daily-backup', [
'databases' => $this->databases,
]);
return $mail;
}
public function toDiscord(): string
{
return "Coolify: Daily backup statuses";
}
public function toTelegram(): array
{
$message = "Coolify: Daily backup statuses";
return [
"message" => $message,
];
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Notifications\Server;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\TelegramChannel;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class ForceDisabled extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 1;
public function __construct(public Server $server)
{
}
public function via(object $notifiable): array
{
$channels = [];
$isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
if ($isDiscordEnabled) {
$channels[] = DiscordChannel::class;
}
if ($isEmailEnabled) {
$channels[] = EmailChannel::class;
}
if ($isTelegramEnabled) {
$channels[] = TelegramChannel::class;
}
return $channels;
}
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject("Coolify: Server ({$this->server->name}) disabled because it is not paid!");
$mail->view('emails.server-force-disabled', [
'name' => $this->server->name,
]);
return $mail;
}
public function toDiscord(): string
{
$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/subsciprtions).";
return $message;
}
public function toTelegram(): array
{
return [
"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/subsciprtions)."
];
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Notifications\Server;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\TelegramChannel;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class ForceEnabled extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 1;
public function __construct(public Server $server)
{
}
public function via(object $notifiable): array
{
$channels = [];
$isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
if ($isDiscordEnabled) {
$channels[] = DiscordChannel::class;
}
if ($isEmailEnabled) {
$channels[] = EmailChannel::class;
}
if ($isTelegramEnabled) {
$channels[] = TelegramChannel::class;
}
return $channels;
}
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject("Coolify: Server ({$this->server->name}) enabled again!");
$mail->view('emails.server-force-enabled', [
'name' => $this->server->name,
]);
return $mail;
}
public function toDiscord(): string
{
$message = "Coolify: Server ({$this->server->name}) enabled again!";
return $message;
}
public function toTelegram(): array
{
return [
"message" => "Coolify: Server ({$this->server->name}) enabled again!"
];
}
}

View File

@@ -2,13 +2,21 @@
namespace App\Providers; namespace App\Providers;
use Illuminate\Auth\Events\Registered; use App\Listeners\MaintenanceModeDisabledNotification;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification; use App\Listeners\MaintenanceModeEnabledNotification;
use Illuminate\Foundation\Events\MaintenanceModeDisabled;
use Illuminate\Foundation\Events\MaintenanceModeEnabled;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider class EventServiceProvider extends ServiceProvider
{ {
protected $listen = [ protected $listen = [
MaintenanceModeEnabled::class => [
MaintenanceModeEnabledNotification::class,
],
MaintenanceModeDisabled::class => [
MaintenanceModeDisabledNotification::class,
],
// Registered::class => [ // Registered::class => [
// SendEmailVerificationNotification::class, // SendEmailVerificationNotification::class,
// ], // ],

View File

@@ -8,18 +8,21 @@ use Illuminate\Support\Collection;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Spatie\Url\Url; use Spatie\Url\Url;
function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pullRequestId = null): Collection function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pullRequestId = null, ?bool $includePullrequests = false): Collection
{ {
$containers = collect([]); $containers = collect([]);
if (!$server->isSwarm()) { if (!$server->isSwarm()) {
$containers = instant_remote_process(["docker ps -a --filter='label=coolify.applicationId={$id}' --format '{{json .}}' "], $server); $containers = instant_remote_process(["docker ps -a --filter='label=coolify.applicationId={$id}' --format '{{json .}}' "], $server);
$containers = format_docker_command_output_to_json($containers); $containers = format_docker_command_output_to_json($containers);
$containers = $containers->map(function ($container) use ($pullRequestId) { $containers = $containers->map(function ($container) use ($pullRequestId, $includePullrequests) {
$labels = data_get($container, 'Labels'); $labels = data_get($container, 'Labels');
if (!str($labels)->contains("coolify.pullRequestId=")) { if (!str($labels)->contains("coolify.pullRequestId=")) {
data_set($container, 'Labels', $labels . ",coolify.pullRequestId={$pullRequestId}"); data_set($container, 'Labels', $labels . ",coolify.pullRequestId={$pullRequestId}");
return $container; return $container;
} }
if ($includePullrequests) {
return $container;
}
if (str($labels)->contains("coolify.pullRequestId=$pullRequestId")) { if (str($labels)->contains("coolify.pullRequestId=$pullRequestId")) {
return $container; return $container;
} }
@@ -423,7 +426,7 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null
'--security-opt', '--security-opt',
'--sysctl', '--sysctl',
'--ulimit', '--ulimit',
'--device' '--device',
]); ]);
$mapping = collect([ $mapping = collect([
'--cap-add' => 'cap_add', '--cap-add' => 'cap_add',
@@ -435,6 +438,7 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null
'--init' => 'init', '--init' => 'init',
'--ulimit' => 'ulimits', '--ulimit' => 'ulimits',
'--privileged' => 'privileged', '--privileged' => 'privileged',
'--ip' => 'ip',
]); ]);
foreach ($matches as $match) { foreach ($matches as $match) {
$option = $match[1]; $option = $match[1];

View File

@@ -110,6 +110,9 @@ function instant_scp(string $source, string $dest, Server $server, $throwError =
} }
function generateSshCommand(Server $server, string $command) function generateSshCommand(Server $server, string $command)
{ {
if ($server->settings->force_disabled) {
throw new \RuntimeException('Server is disabled.');
}
$user = $server->user; $user = $server->user;
$port = $server->port; $port = $server->port;
$privateKeyLocation = savePrivateKeyToFs($server); $privateKeyLocation = savePrivateKeyToFs($server);

View File

@@ -33,6 +33,11 @@ use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\Support\Stringable; use Illuminate\Support\Stringable;
use Lcobucci\JWT\Encoding\ChainedFormatter;
use Lcobucci\JWT\Encoding\JoseEncoder;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Signer\Hmac\Sha256;
use Lcobucci\JWT\Token\Builder;
use Poliander\Cron\CronExpression; use Poliander\Cron\CronExpression;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
use phpseclib3\Crypt\RSA; use phpseclib3\Crypt\RSA;
@@ -625,7 +630,6 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
} }
} }
$definedNetwork = collect([$resource->uuid]); $definedNetwork = collect([$resource->uuid]);
$services = collect($services)->map(function ($service, $serviceName) use ($topLevelVolumes, $topLevelNetworks, $definedNetwork, $isNew, $generatedServiceFQDNS, $resource) { $services = collect($services)->map(function ($service, $serviceName) use ($topLevelVolumes, $topLevelNetworks, $definedNetwork, $isNew, $generatedServiceFQDNS, $resource) {
$serviceVolumes = collect(data_get($service, 'volumes', [])); $serviceVolumes = collect(data_get($service, 'volumes', []));
$servicePorts = collect(data_get($service, 'ports', [])); $servicePorts = collect(data_get($service, 'ports', []));
@@ -927,6 +931,13 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$savedService->fqdn = $fqdn; $savedService->fqdn = $fqdn;
$savedService->save(); $savedService->save();
} }
EnvironmentVariable::create([
'key' => $key,
'value' => $fqdn,
'is_build_time' => false,
'service_id' => $resource->id,
'is_preview' => false,
]);
} }
// data_forget($service, "environment.$variableName"); // data_forget($service, "environment.$variableName");
// $yaml = data_forget($yaml, "services.$serviceName.environment.$variableName"); // $yaml = data_forget($yaml, "services.$serviceName.environment.$variableName");
@@ -978,7 +989,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
} }
} }
} else { } else {
$generatedValue = generateEnvValue($command); $generatedValue = generateEnvValue($command, $resource);
if (!$foundEnv) { if (!$foundEnv) {
EnvironmentVariable::create([ EnvironmentVariable::create([
'key' => $key, 'key' => $key,
@@ -1394,7 +1405,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
]); ]);
} }
} else { } else {
$generatedValue = generateEnvValue($command); $generatedValue = generateEnvValue($command, $service);
if (!$foundEnv) { if (!$foundEnv) {
EnvironmentVariable::create([ EnvironmentVariable::create([
'key' => $key, 'key' => $key,
@@ -1570,7 +1581,7 @@ function parseEnvVariable(Str|string $value)
'port' => $port, 'port' => $port,
]; ];
} }
function generateEnvValue(string $command) function generateEnvValue(string $command, Service $service)
{ {
switch ($command) { switch ($command) {
case 'PASSWORD': case 'PASSWORD':
@@ -1591,6 +1602,46 @@ function generateEnvValue(string $command)
case 'USER': case 'USER':
$generatedValue = Str::random(16); $generatedValue = Str::random(16);
break; break;
case 'SUPABASEANON':
$signingKey = $service->environment_variables()->where('key', 'SERVICE_PASSWORD_JWT')->first();
if (is_null($signingKey)) {
return;
} else {
$signingKey = $signingKey->value;
}
$key = InMemory::plainText($signingKey);
$algorithm = new Sha256();
$tokenBuilder = (new Builder(new JoseEncoder(), ChainedFormatter::default()));
$now = new DateTimeImmutable();
$now = $now->setTime($now->format('H'), $now->format('i'));
$token = $tokenBuilder
->issuedBy('supabase')
->issuedAt($now)
->expiresAt($now->modify('+100 year'))
->withClaim('role', 'anon')
->getToken($algorithm, $key);
$generatedValue = $token->toString();
break;
case 'SUPABASESERVICE':
$signingKey = $service->environment_variables()->where('key', 'SERVICE_PASSWORD_JWT')->first();
if (is_null($signingKey)) {
return;
} else {
$signingKey = $signingKey->value;
}
$key = InMemory::plainText($signingKey);
$algorithm = new Sha256();
$tokenBuilder = (new Builder(new JoseEncoder(), ChainedFormatter::default()));
$now = new DateTimeImmutable();
$now = $now->setTime($now->format('H'), $now->format('i'));
$token = $tokenBuilder
->issuedBy('supabase')
->issuedAt($now)
->expiresAt($now->modify('+100 year'))
->withClaim('role', 'service_role')
->getToken($algorithm, $key);
$generatedValue = $token->toString();
break;
default: default:
$generatedValue = Str::random(16); $generatedValue = Str::random(16);
break; break;

View File

@@ -35,6 +35,13 @@ return [
'throw' => false, 'throw' => false,
], ],
'webhooks-during-maintenance' => [
'driver' => 'local',
'root' => storage_path('app/webhooks-during-maintenance'),
'visibility' => 'private',
'throw' => false,
],
'public' => [ 'public' => [
'driver' => 'local', 'driver' => 'local',
'root' => storage_path('app/public'), 'root' => storage_path('app/public'),

View File

@@ -7,7 +7,7 @@ return [
// The release version of your application // The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => '4.0.0-beta.224', 'release' => '4.0.0-beta.231',
// When left empty or `null` the Laravel environment will be used // When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'), 'environment' => config('app.env'),

View File

@@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.224'; return '4.0.0-beta.231';

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('teams', function (Blueprint $table) {
$table->integer('custom_server_limit')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('teams', function (Blueprint $table) {
$table->dropColumn('custom_server_limit');
});
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('server_settings', function (Blueprint $table) {
$table->boolean('force_disabled')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('server_settings', function (Blueprint $table) {
$table->dropColumn('force_disabled');
});
}
};

View File

@@ -18,7 +18,6 @@ class DatabaseSeeder extends Seeder
ProjectSeeder::class, ProjectSeeder::class,
ProjectSettingSeeder::class, ProjectSettingSeeder::class,
EnvironmentSeeder::class, EnvironmentSeeder::class,
TeamEnvironmentVariableSeeder::class,
StandaloneDockerSeeder::class, StandaloneDockerSeeder::class,
SwarmDockerSeeder::class, SwarmDockerSeeder::class,
KubernetesSeeder::class, KubernetesSeeder::class,

View File

@@ -12,6 +12,7 @@ services:
- /data/coolify/databases:/var/www/html/storage/app/databases - /data/coolify/databases:/var/www/html/storage/app/databases
- /data/coolify/services:/var/www/html/storage/app/services - /data/coolify/services:/var/www/html/storage/app/services
- /data/coolify/backups:/var/www/html/storage/app/backups - /data/coolify/backups:/var/www/html/storage/app/backups
- /data/coolify/webhooks-during-maintenance:/var/www/html/storage/app/webhooks-during-maintenance
environment: environment:
- APP_ID - APP_ID
- APP_ENV=production - APP_ENV=production

View File

@@ -26,6 +26,7 @@ services:
- ./databases:/var/www/html/storage/app/databases - ./databases:/var/www/html/storage/app/databases
- ./services:/var/www/html/storage/app/services - ./services:/var/www/html/storage/app/services
- ./backups:/var/www/html/storage/app/backups - ./backups:/var/www/html/storage/app/backups
- ./webhooks-during-maintenance:/var/www/html/storage/app/webhooks-during-maintenance
env_file: env_file:
- .env - .env
environment: environment:

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="white" stroke="white" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14m-6 6l6-6m-6-6l6 6"/>
</svg>

After

Width:  |  Height:  |  Size: 203 B

15
public/svgs/supabase.svg Normal file
View File

@@ -0,0 +1,15 @@
<svg width="109" height="113" viewBox="0 0 109 113" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z" fill="url(#paint0_linear)"/>
<path d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z" fill="url(#paint1_linear)" fill-opacity="0.2"/>
<path d="M45.317 2.07103C48.1765 -1.53037 53.9745 0.442937 54.0434 5.041L54.4849 72.2922H9.83113C1.64038 72.2922 -2.92775 62.8321 2.1655 56.4175L45.317 2.07103Z" fill="#3ECF8E"/>
<defs>
<linearGradient id="paint0_linear" x1="53.9738" y1="54.974" x2="94.1635" y2="71.8295" gradientUnits="userSpaceOnUse">
<stop stop-color="#249361"/>
<stop offset="1" stop-color="#3ECF8E"/>
</linearGradient>
<linearGradient id="paint1_linear" x1="36.1558" y1="30.578" x2="54.4844" y2="65.0806" gradientUnits="userSpaceOnUse">
<stop/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,22 @@
@props(['closable' => true])
<div x-data="{
bannerVisible: false,
bannerVisibleAfter: 100,
}" x-show="bannerVisible" x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="-translate-y-10" x-transition:enter-end="translate-y-0"
x-transition:leave="transition ease-in duration-100" x-transition:leave-start="translate-y-0"
x-transition:leave-end="-translate-y-10" x-init="setTimeout(() => { bannerVisible = true }, bannerVisibleAfter);"
class="relative z-50 w-full py-2 mx-auto duration-100 ease-out shadow-sm bg-coolgray-100 sm:py-0 sm:h-14" x-cloak>
<div class="flex items-center justify-between h-full px-3">
{{ $slot }}
@if ($closable)
<button @click="bannerVisible=false"
class="flex items-center flex-shrink-0 translate-x-1 ease-out duration-150 justify-center w-6 h-6 p-1.5 text-neutral-200 rounded-full hover:bg-coolgray-500">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-full h-full">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
@endif
</div>
</div>

View File

@@ -0,0 +1 @@
<img class="inline-flex w-4 h-4" src="{{ asset('svgs/internal-link.svg') }}">

View File

@@ -1,6 +1,6 @@
<div class="flex flex-col items-center justify-center h-screen"> <div class="flex flex-col items-center justify-center h-screen">
<span class="text-xl font-bold text-white">You have reached the limit of {{ $name }} you can create.</span> <span class="text-xl font-bold text-white">You have reached the limit of {{ $name }} you can create.</span>
<span>Please <a class="text-white underline "href="{{ route('team.index') }}">upgrade your <span>Please <a class="text-white underline "href="{{ route('subscription.show') }}">upgrade your
subscription</a> to create more subscription</a> to create more
{{ $name }}.</span> {{ $name }}.</span>
</div> </div>

View File

@@ -188,21 +188,21 @@
</div> </div>
<div class="pt-16 lg:px-8 lg:pt-0 xl:px-14"> <div class="pt-16 lg:px-8 lg:pt-0 xl:px-14">
<h3 id="tier-ultimate" class="text-base font-semibold leading-7 text-white">Ultimate</h3> <h3 id="tier-ultimate" class="text-base font-semibold leading-7 text-white">Ultimate</h3>
<p class="flex items-baseline mt-6 gap-x-1"> <p class="flex items-baseline mt-6 gap-x-1">
<span x-show="selected === 'monthly'" x-cloak> <span x-show="selected === 'monthly'" x-cloak>
<span class="text-4xl font-bold tracking-tight text-white">$?</span> <span class="text-4xl font-bold tracking-tight text-white">Custom</span>
<span class="text-sm font-semibold leading-6 ">/month + VAT</span> {{-- <span class="text-sm font-semibold leading-6 ">pay-as-you-go</span> --}}
</span> </span>
<span x-show="selected === 'yearly'" x-cloak> <span x-show="selected === 'yearly'" x-cloak>
<span class="text-4xl font-bold tracking-tight text-white">$?</span> <span class="text-4xl font-bold tracking-tight text-white">Custom</span>
<span class="text-sm font-semibold leading-6 ">/month + VAT</span> {{-- <span class="text-sm font-semibold leading-6 ">/month + VAT</span> --}}
</span> </span>
</p> </p>
<span x-show="selected === 'monthly'" x-cloak> <span x-show="selected === 'monthly'" x-cloak>
<span>billed monthly</span> <span>pay-as-you-go</span>
</span> </span>
<span x-show="selected === 'yearly'" x-cloak> <span x-show="selected === 'yearly'" x-cloak>
<span>billed annually</span> <span>pay-as-you-go</span>
</span> </span>
@if ($showSubscribeButtons) @if ($showSubscribeButtons)
@isset($ultimate) @isset($ultimate)
@@ -219,7 +219,7 @@
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
clip-rule="evenodd" /> clip-rule="evenodd" />
</svg> </svg>
Connect <span class="px-1 font-bold text-white">unlimited</span> servers Connect <span class="px-1 font-bold text-white">10+</span> servers
</li> </li>
<li class="flex gap-x-3"> <li class="flex gap-x-3">

View File

@@ -1,24 +1,18 @@
<div> <div class="flex h-full pr-4">
@if ($server->isFunctional()) <div class="flex flex-col w-48 gap-4 min-w-fit">
<div class="flex h-full pr-4"> <a class="{{ request()->routeIs('server.proxy') ? 'text-white' : '' }}"
<div class="flex flex-col w-48 gap-4 min-w-fit"> href="{{ route('server.proxy', $parameters) }}">
<a class="{{ request()->routeIs('server.proxy') ? 'text-white' : '' }}" <button>Configuration</button>
href="{{ route('server.proxy', $parameters) }}"> </a>
<button>Configuration</button> @if (data_get($server, 'proxy.type') !== 'NONE')
</a> <a class="{{ request()->routeIs('server.proxy.dynamic-confs') ? 'text-white' : '' }}"
@if (data_get($server, 'proxy.type') !== 'NONE') href="{{ route('server.proxy.dynamic-confs', $parameters) }}">
<a class="{{ request()->routeIs('server.proxy.dynamic-confs') ? 'text-white' : '' }}" <button>Dynamic Configurations</button>
href="{{ route('server.proxy.dynamic-confs', $parameters) }}"> </a>
<button>Dynamic Configurations</button> <a class="{{ request()->routeIs('server.proxy.logs') ? 'text-white' : '' }}"
</a> href="{{ route('server.proxy.logs', $parameters) }}">
<a class="{{ request()->routeIs('server.proxy.logs') ? 'text-white' : '' }}" <button>Logs</button>
href="{{ route('server.proxy.logs', $parameters) }}"> </a>
<button>Logs</button> @endif
</a> </div>
@endif
</div>
</div>
@else
<div>Server is not validated. Validate first.</div>
@endif
</div> </div>

View File

@@ -0,0 +1,19 @@
<x-emails.layout>
@foreach ($databases as $database_name => $databases)
@if(data_get($databases,'failed_count') > 0)
<div style="color:red">
"{{ $database_name }}" backups: There were some failed backups. Please login and check the logs for more details.
</div>
@else
"{{ $database_name }}" backups: All backups were successful.
@endif
@endforeach
</x-emails.layout>

View File

@@ -0,0 +1,5 @@
<x-emails.layout>
Your server ({{ $name }}) disabled because it is not paid! All automations and integrations are stopped.
Please update your subscription to enable the server again [here](https://app.coolify.io/subsciprtions).
</x-emails.layout>

View File

@@ -0,0 +1,3 @@
<x-emails.layout>
Your server ({{ $name }}) is enabled again!
</x-emails.layout>

View File

@@ -1,5 +1,5 @@
<x-emails.layout> <x-emails.layout>
Your trial ends soon. Please update payment details [here]({{ $stripeCustomerPortal }}), Your trial ends soon. Please update payment details [here]({{ $stripeCustomerPortal }}),
Your servers & deployed resources will be untouched, but you won't be able to deploy new resources and lost all automations and integrations. Your servers & deployed resources will be untouched, but you won't be able to deploy new resources and lose all automations and integrations.
</x-emails.layout> </x-emails.layout>

View File

@@ -7,7 +7,7 @@
<magic-bar></magic-bar> <magic-bar></magic-bar>
</div> </div>
@endpersist @endpersist
<livewire:sponsorship /> <livewire:layout-popups />
@auth @auth
<livewire:realtime-connection /> <livewire:realtime-connection />
@endauth @endauth

View File

@@ -4,7 +4,7 @@
{{ auth()->user()->name }} {{ auth()->user()->name }}
<h3 class="pt-4">Users</h3> <h3 class="pt-4">Users</h3>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<div class="w-96 box" wire:click="switchUser('0')"> <div class="text-white cursor-pointer w-96 box-without-bg bg-coollabs-100" wire:click="switchUser('0')">
Root Root
</div> </div>
@foreach ($users as $user) @foreach ($users as $user)

View File

@@ -16,7 +16,6 @@
<button class="text-white btn-link">{{ data_get($docker, 'network') }} </button> <button class="text-white btn-link">{{ data_get($docker, 'network') }} </button>
</a> </a>
@empty @empty
<div class="">N/A</div>
@endforelse @endforelse
@forelse ($server->swarmDockers as $docker) @forelse ($server->swarmDockers as $docker)
<a <a
@@ -24,7 +23,6 @@
<button class="text-white btn-link">{{ data_get($docker, 'network') }} </button> <button class="text-white btn-link">{{ data_get($docker, 'network') }} </button>
</a> </a>
@empty @empty
<div class="">N/A</div>
@endforelse @endforelse
</div> </div>
<div class="pt-2"> <div class="pt-2">

View File

@@ -11,4 +11,13 @@
</div> </div>
</div> </div>
@endif @endif
@if (currentTeam()->serverOverflow())
<x-banner :closable=false>
<div><span class="font-bold text-red-500">WARNING:</span> The number of active servers exceeds the limit
covered by your payment. If not resolved, some of your servers <span class="font-bold text-red-500">will
be deactivated</span>. Visit <a href="{{ route('subscription.show') }}"
class="text-white underline">/subscription</a> to update your subscription or remove some servers.
</div>
</x-banner>
@endif
</div> </div>

View File

@@ -4,8 +4,8 @@
<h2>Advanced</h2> <h2>Advanced</h2>
</div> </div>
<div>Advanced configuration for your application.</div> <div>Advanced configuration for your application.</div>
<div class="flex flex-col pt-4 "> <div class="flex flex-col pt-4 w-96">
<h4>General</h4> <h3>General</h3>
@if ($application->git_based()) @if ($application->git_based())
<x-forms.checkbox helper="Automatically deploy new commits based on Git webhooks." instantSave <x-forms.checkbox helper="Automatically deploy new commits based on Git webhooks." instantSave
id="application.settings.is_auto_deploy_enabled" label="Auto Deploy" /> id="application.settings.is_auto_deploy_enabled" label="Auto Deploy" />
@@ -20,50 +20,52 @@
helper="The deployed container will have the same name ({{ $application->uuid }}). <span class='font-bold text-warning'>You will lose the rolling update feature!</span>" helper="The deployed container will have the same name ({{ $application->uuid }}). <span class='font-bold text-warning'>You will lose the rolling update feature!</span>"
instantSave id="application.settings.is_consistent_container_name_enabled" instantSave id="application.settings.is_consistent_container_name_enabled"
label="Consistent Container Names" /> label="Consistent Container Names" />
<h4>Logs</h4> <h3>Logs</h3>
@if (!$application->settings->is_raw_compose_deployment_enabled) @if (!$application->settings->is_raw_compose_deployment_enabled)
<x-forms.checkbox helper="Drain logs to your configured log drain endpoint in your Server settings." <x-forms.checkbox helper="Drain logs to your configured log drain endpoint in your Server settings."
instantSave id="application.settings.is_log_drain_enabled" label="Drain Logs" /> instantSave id="application.settings.is_log_drain_enabled" label="Drain Logs" />
@endif @endif
@if ($application->git_based()) @if ($application->git_based())
<h4>Git</h4> <h3>Git</h3>
<x-forms.checkbox instantSave id="application.settings.is_git_submodules_enabled" label="Git Submodules" <x-forms.checkbox instantSave id="application.settings.is_git_submodules_enabled" label="Submodules"
helper="Allow Git Submodules during build process." /> helper="Allow Git Submodules during build process." />
<x-forms.checkbox instantSave id="application.settings.is_git_lfs_enabled" label="Git LFS" <x-forms.checkbox instantSave id="application.settings.is_git_lfs_enabled" label="LFS"
helper="Allow Git LFS during build process." /> helper="Allow Git LFS during build process." />
@endif @endif
<h4>GPU</h4>
<form wire:submit="submit">
@if ($application->build_pack !== 'dockercompose')
<div class="flex gap-2">
<x-forms.checkbox
helper="Enable GPU usage for this application. More info <a href='https://docs.docker.com/compose/gpu-support/' class='text-white underline' target='_blank'>here</a>."
instantSave id="application.settings.is_gpu_enabled" label="Attach GPU" />
@if ($application->settings->is_gpu_enabled)
<x-forms.button type="submiot">Save</x-forms.button>
@endif
</div>
@endif
@if ($application->settings->is_gpu_enabled)
<div class="flex flex-col w-full gap-2 p-2 xl:flex-row">
<x-forms.input label="GPU Driver" id="application.settings.gpu_driver"> </x-forms.input>
<x-forms.input label="GPU Count" placeholder="empty means use all GPUs"
id="application.settings.gpu_count"> </x-forms.input>
<x-forms.input label="GPU Device Ids" placeholder="0,2"
helper="Comma separated list of device ids. More info <a href='https://docs.docker.com/compose/gpu-support/#access-specific-devices' class='text-white underline' target='_blank'>here</a>."
id="application.settings.gpu_device_ids"> </x-forms.input>
</div>
<div class="px-2">
<x-forms.textarea label="GPU Options" id="application.settings.gpu_options">
</x-forms.textarea>
</div>
@endif
</form>
{{-- <x-forms.checkbox disabled instantSave id="is_dual_cert" label="Dual Certs?" /> {{-- <x-forms.checkbox disabled instantSave id="is_dual_cert" label="Dual Certs?" />
<x-forms.checkbox disabled instantSave id="is_custom_ssl" label="Is Custom SSL?" /> <x-forms.checkbox disabled instantSave id="is_custom_ssl" label="Is Custom SSL?" />
<x-forms.checkbox disabled instantSave id="is_http2" label="Is Http2?" /> --}} <x-forms.checkbox disabled instantSave id="is_http2" label="Is Http2?" /> --}}
</div> </div>
<h3>GPU</h3>
<form wire:submit="submit">
@if ($application->build_pack !== 'dockercompose')
<div class="w-96">
<x-forms.checkbox
helper="Enable GPU usage for this application. More info <a href='https://docs.docker.com/compose/gpu-support/' class='text-white underline' target='_blank'>here</a>."
instantSave id="application.settings.is_gpu_enabled" label="Attach GPU" />
@if ($application->settings->is_gpu_enabled)
<h5>GPU Settings</h5>
<x-forms.button type="submit">Save</x-forms.button>
@endif
</div>
@endif
@if ($application->settings->is_gpu_enabled)
<div class="flex flex-col w-full gap-2 p-2 xl:flex-row">
<x-forms.input label="GPU Driver" id="application.settings.gpu_driver"> </x-forms.input>
<x-forms.input label="GPU Count" placeholder="empty means use all GPUs"
id="application.settings.gpu_count"> </x-forms.input>
<x-forms.input label="GPU Device Ids" placeholder="0,2"
helper="Comma separated list of device ids. More info <a href='https://docs.docker.com/compose/gpu-support/#access-specific-devices' class='text-white underline' target='_blank'>here</a>."
id="application.settings.gpu_device_ids"> </x-forms.input>
</div>
<div class="px-2">
<x-forms.textarea label="GPU Options" id="application.settings.gpu_options">
</x-forms.textarea>
</div>
@endif
</form>
</div> </div>
</div> </div>

View File

@@ -27,10 +27,17 @@
<a :class="activeTab === 'source' && 'text-white'" <a :class="activeTab === 'source' && 'text-white'"
@click.prevent="activeTab = 'source'; window.location.hash = 'source'" href="#">Source</a> @click.prevent="activeTab = 'source'; window.location.hash = 'source'" href="#">Source</a>
@endif @endif
<a :class="activeTab === 'servers' && 'text-white'" <a :class="activeTab === 'servers' && 'text-white'" class="flex items-center gap-2"
@click.prevent="activeTab = 'servers'; window.location.hash = 'servers'" href="#">Servers @click.prevent="activeTab = 'servers'; window.location.hash = 'servers'" href="#">Servers
@if (str($application->status)->contains('degraded'))
<span title="Some servers are unavailable">
<svg class="w-4 h-4 text-error" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor"
d="M240.26 186.1L152.81 34.23a28.74 28.74 0 0 0-49.62 0L15.74 186.1a27.45 27.45 0 0 0 0 27.71A28.31 28.31 0 0 0 40.55 228h174.9a28.31 28.31 0 0 0 24.79-14.19a27.45 27.45 0 0 0 .02-27.71m-20.8 15.7a4.46 4.46 0 0 1-4 2.2H40.55a4.46 4.46 0 0 1-4-2.2a3.56 3.56 0 0 1 0-3.73L124 46.2a4.77 4.77 0 0 1 8 0l87.44 151.87a3.56 3.56 0 0 1 .02 3.73M116 136v-32a12 12 0 0 1 24 0v32a12 12 0 0 1-24 0m28 40a16 16 0 1 1-16-16a16 16 0 0 1 16 16" />
</svg>
</span>
@endif
</a> </a>
<a :class="activeTab === 'scheduled-tasks' && 'text-white'" <a :class="activeTab === 'scheduled-tasks' && 'text-white'"
@click.prevent="activeTab = 'scheduled-tasks'; window.location.hash = 'scheduled-tasks'" @click.prevent="activeTab = 'scheduled-tasks'; window.location.hash = 'scheduled-tasks'"
href="#">Scheduled Tasks href="#">Scheduled Tasks

View File

@@ -52,8 +52,8 @@
@foreach (decode_remote_command_output($application_deployment_queue) as $line) @foreach (decode_remote_command_output($application_deployment_queue) as $line)
<div @class([ <div @class([
'font-mono', 'font-mono',
'text-warning' => $line['hidden'], 'text-warning whitespace-pre-line' => $line['hidden'],
'text-red-500' => $line['type'] == 'stderr', 'text-red-500 whitespace-pre-line' => $line['type'] == 'stderr',
])>[{{ $line['timestamp'] }}] @if ($line['hidden']) ])>[{{ $line['timestamp'] }}] @if ($line['hidden'])
<br>COMMAND: <br>{{ $line['command'] }} <br><br>OUTPUT: <br>COMMAND: <br>{{ $line['command'] }} <br><br>OUTPUT:
@endif @if (str($line['output'])->contains('http://') || str($line['output'])->contains('https://')) @endif @if (str($line['output'])->contains('http://') || str($line['output'])->contains('https://'))

View File

@@ -8,7 +8,8 @@
@if ($isConfigurationChanged && !is_null($application->config_hash) && !$application->isExited()) @if ($isConfigurationChanged && !is_null($application->config_hash) && !$application->isExited())
<div title="Configuration not applied to the running application. You need to redeploy."> <div title="Configuration not applied to the running application. You need to redeploy.">
<svg class="w-6 h-6 text-warning" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg"> <svg class="w-6 h-6 text-warning" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" d="M240.26 186.1L152.81 34.23a28.74 28.74 0 0 0-49.62 0L15.74 186.1a27.45 27.45 0 0 0 0 27.71A28.31 28.31 0 0 0 40.55 228h174.9a28.31 28.31 0 0 0 24.79-14.19a27.45 27.45 0 0 0 .02-27.71m-20.8 15.7a4.46 4.46 0 0 1-4 2.2H40.55a4.46 4.46 0 0 1-4-2.2a3.56 3.56 0 0 1 0-3.73L124 46.2a4.77 4.77 0 0 1 8 0l87.44 151.87a3.56 3.56 0 0 1 .02 3.73M116 136v-32a12 12 0 0 1 24 0v32a12 12 0 0 1-24 0m28 40a16 16 0 1 1-16-16a16 16 0 0 1 16 16"/> <path fill="currentColor"
d="M240.26 186.1L152.81 34.23a28.74 28.74 0 0 0-49.62 0L15.74 186.1a27.45 27.45 0 0 0 0 27.71A28.31 28.31 0 0 0 40.55 228h174.9a28.31 28.31 0 0 0 24.79-14.19a27.45 27.45 0 0 0 .02-27.71m-20.8 15.7a4.46 4.46 0 0 1-4 2.2H40.55a4.46 4.46 0 0 1-4-2.2a3.56 3.56 0 0 1 0-3.73L124 46.2a4.77 4.77 0 0 1 8 0l87.44 151.87a3.56 3.56 0 0 1 .02 3.73M116 136v-32a12 12 0 0 1 24 0v32a12 12 0 0 1-24 0m28 40a16 16 0 1 1-16-16a16 16 0 0 1 16 16" />
</svg> </svg>
</div> </div>
@endif @endif
@@ -100,11 +101,15 @@
<x-forms.input id="application.docker_registry_image_tag" label="Docker Image Tag" /> <x-forms.input id="application.docker_registry_image_tag" label="Docker Image Tag" />
@endif @endif
@else @else
@if ($application->destination->server->isSwarm() || $application->additional_servers->count() > 0) @if (
<x-forms.input id="application.docker_registry_image_name" required label="Docker Image" /> $application->destination->server->isSwarm() ||
$application->additional_servers->count() > 0 ||
$application->settings->is_build_server_enabled)
<x-forms.input id="application.docker_registry_image_name" required label="Docker Image"
placeholder="Required!" />
<x-forms.input id="application.docker_registry_image_tag" <x-forms.input id="application.docker_registry_image_tag"
helper="If set, it will tag the built image with this tag too. <br><br>Example: If you set it to 'latest', it will push the image with the commit sha tag + with the latest tag." helper="If set, it will tag the built image with this tag too. <br><br>Example: If you set it to 'latest', it will push the image with the commit sha tag + with the latest tag."
label="Docker Image Tag" /> placeholder="Empty means latest will be used." label="Docker Image Tag" />
@else @else
<x-forms.input id="application.docker_registry_image_name" <x-forms.input id="application.docker_registry_image_name"
helper="Empty means it won't push the image to a docker registry." helper="Empty means it won't push the image to a docker registry."

View File

@@ -86,15 +86,21 @@
Redeploy Redeploy
@endif @endif
</x-forms.button> </x-forms.button>
<x-forms.button class="bg-coolgray-500"
wire:click="stop({{ data_get($preview, 'pull_request_id') }})">Remove Preview
</x-forms.button>
<a <a
href="{{ route('project.application.deployment.index', [...$parameters, 'pull_request_id' => data_get($preview, 'pull_request_id')]) }}"> href="{{ route('project.application.deployment.index', [...$parameters, 'pull_request_id' => data_get($preview, 'pull_request_id')]) }}">
<x-forms.button class="bg-coolgray-500"> <x-forms.button class="bg-coolgray-500">
Get Deployment Logs Deployment Logs
</x-forms.button> </x-forms.button>
</a> </a>
<a
href="{{ route('project.application.logs', [...$parameters, 'pull_request_id' => data_get($preview, 'pull_request_id')]) }}">
<x-forms.button class="bg-coolgray-500">
Application Logs
</x-forms.button>
</a>
<x-forms.button isError class="bg-coolgray-500"
wire:click="stop({{ data_get($preview, 'pull_request_id') }})">Delete
</x-forms.button>
</div> </div>
</div> </div>
@endforeach @endforeach

View File

@@ -1,4 +1,4 @@
<nav wire:poll.30000ms="check_status"> <nav wire:poll.5000ms="check_status">
<x-resources.breadcrumbs :resource="$database" :parameters="$parameters" /> <x-resources.breadcrumbs :resource="$database" :parameters="$parameters" />
<x-databases.navbar :database="$database" :parameters="$parameters" /> <x-databases.navbar :database="$database" :parameters="$parameters" />
</nav> </nav>

View File

@@ -45,12 +45,12 @@
class="items-center justify-center box">+ Add New Resource</a> class="items-center justify-center box">+ Add New Resource</a>
@else @else
<div x-data="searchComponent()"> <div x-data="searchComponent()">
<x-forms.input placeholder="Search for name, fqdn..." class="w-full" x-model="search" /> <x-forms.input autofocus="true" placeholder="Search for name, fqdn..." class="w-full" x-model="search" />
<div class="grid grid-cols-1 gap-4 pt-4 lg:grid-cols-2 xl:grid-cols-3"> <div class="grid grid-cols-1 gap-4 pt-4 lg:grid-cols-2 xl:grid-cols-3">
<template x-for="item in filteredApplications" :key="item.id"> <template x-for="item in filteredApplications" :key="item.id">
<span> <span>
<a class="h-24 box group" :href="item.hrefLink"> <a class="h-24 box group" :href="item.hrefLink">
<div class="flex flex-col mx-6"> <div class="flex flex-col w-full px-4 mx-2">
<div class="flex gap-2"> <div class="flex gap-2">
<div class="pb-2 font-bold text-white" x-text="item.name"></div> <div class="pb-2 font-bold text-white" x-text="item.name"></div>
<template x-if="item.status.startsWith('running')"> <template x-if="item.status.startsWith('running')">
@@ -66,8 +66,8 @@
<div title="degraded" class="mt-1 bg-warning badge badge-xs"></div> <div title="degraded" class="mt-1 bg-warning badge badge-xs"></div>
</template> </template>
</div> </div>
<div class="description" x-text="item.description"></div> <div class="max-w-full truncate description" x-text="item.description"></div>
<div class="description break-all" x-text="item.fqdn"></div> <div class="max-w-full truncate description" x-text="item.fqdn"></div>
</div> </div>
</a> </a>
<div class="flex gap-1 pt-1 group-hover:text-white group min-h-6"> <div class="flex gap-1 pt-1 group-hover:text-white group min-h-6">
@@ -83,7 +83,7 @@
<template x-for="item in filteredPostgresqls" :key="item.id"> <template x-for="item in filteredPostgresqls" :key="item.id">
<span> <span>
<a class="h-24 box group" :href="item.hrefLink"> <a class="h-24 box group" :href="item.hrefLink">
<div class="flex flex-col mx-6"> <div class="flex flex-col px-4 mx-2">
<div class="flex gap-2"> <div class="flex gap-2">
<div class="pb-2 font-bold text-white" x-text="item.name"></div> <div class="pb-2 font-bold text-white" x-text="item.name"></div>
<template x-if="item.status.startsWith('running')"> <template x-if="item.status.startsWith('running')">
@@ -99,7 +99,7 @@
<div title="degraded" class="mt-1 bg-warning badge badge-xs"></div> <div title="degraded" class="mt-1 bg-warning badge badge-xs"></div>
</template> </template>
</div> </div>
<div class="description" x-text="item.description"></div> <div class="max-w-full truncate description" x-text="item.description"></div>
</div> </div>
</a> </a>
<div class="flex gap-1 pt-1 group-hover:text-white group min-h-6"> <div class="flex gap-1 pt-1 group-hover:text-white group min-h-6">
@@ -115,7 +115,7 @@
<template x-for="item in filteredRedis" :key="item.id"> <template x-for="item in filteredRedis" :key="item.id">
<span> <span>
<a class="h-24 box group" :href="item.hrefLink"> <a class="h-24 box group" :href="item.hrefLink">
<div class="flex flex-col mx-6"> <div class="flex flex-col px-4 mx-2">
<div class="flex gap-2"> <div class="flex gap-2">
<div class="pb-2 font-bold text-white" x-text="item.name"></div> <div class="pb-2 font-bold text-white" x-text="item.name"></div>
<template x-if="item.status.startsWith('running')"> <template x-if="item.status.startsWith('running')">
@@ -131,7 +131,7 @@
<div title="degraded" class="mt-1 bg-warning badge badge-xs"></div> <div title="degraded" class="mt-1 bg-warning badge badge-xs"></div>
</template> </template>
</div> </div>
<div class="description" x-text="item.description"></div> <div class="max-w-full truncate description" x-text="item.description"></div>
</div> </div>
</a> </a>
<div class="flex gap-1 pt-1 group-hover:text-white group min-h-6"> <div class="flex gap-1 pt-1 group-hover:text-white group min-h-6">
@@ -147,7 +147,7 @@
<template x-for="item in filteredMongodbs" :key="item.id"> <template x-for="item in filteredMongodbs" :key="item.id">
<span> <span>
<a class="h-24 box group" :href="item.hrefLink"> <a class="h-24 box group" :href="item.hrefLink">
<div class="flex flex-col mx-6"> <div class="flex flex-col px-4 mx-2">
<div class="flex gap-2"> <div class="flex gap-2">
<div class="pb-2 font-bold text-white" x-text="item.name"></div> <div class="pb-2 font-bold text-white" x-text="item.name"></div>
<template x-if="item.status.startsWith('running')"> <template x-if="item.status.startsWith('running')">
@@ -163,7 +163,7 @@
<div title="degraded" class="mt-1 bg-warning badge badge-xs"></div> <div title="degraded" class="mt-1 bg-warning badge badge-xs"></div>
</template> </template>
</div> </div>
<div class="description" x-text="item.description"></div> <div class="max-w-full truncate description" x-text="item.description"></div>
</div> </div>
</a> </a>
<div class="flex gap-1 pt-1 group-hover:text-white group min-h-6"> <div class="flex gap-1 pt-1 group-hover:text-white group min-h-6">
@@ -179,7 +179,7 @@
<template x-for="item in filteredMysqls" :key="item.id"> <template x-for="item in filteredMysqls" :key="item.id">
<span> <span>
<a class="h-24 box group" :href="item.hrefLink"> <a class="h-24 box group" :href="item.hrefLink">
<div class="flex flex-col mx-6"> <div class="flex flex-col px-4 mx-2">
<div class="flex gap-2"> <div class="flex gap-2">
<div class="pb-2 font-bold text-white" x-text="item.name"></div> <div class="pb-2 font-bold text-white" x-text="item.name"></div>
<template x-if="item.status.startsWith('running')"> <template x-if="item.status.startsWith('running')">
@@ -195,7 +195,7 @@
<div title="degraded" class="mt-1 bg-warning badge badge-xs"></div> <div title="degraded" class="mt-1 bg-warning badge badge-xs"></div>
</template> </template>
</div> </div>
<div class="description" x-text="item.description"></div> <div class="max-w-full truncate description" x-text="item.description"></div>
</div> </div>
</a> </a>
<div class="flex gap-1 pt-1 group-hover:text-white group min-h-6"> <div class="flex gap-1 pt-1 group-hover:text-white group min-h-6">
@@ -211,7 +211,7 @@
<template x-for="item in filteredMariadbs" :key="item.id"> <template x-for="item in filteredMariadbs" :key="item.id">
<span> <span>
<a class="h-24 box group" :href="item.hrefLink"> <a class="h-24 box group" :href="item.hrefLink">
<div class="flex flex-col mx-6"> <div class="flex flex-col px-4 mx-2">
<div class="flex gap-2"> <div class="flex gap-2">
<div class="pb-2 font-bold text-white" x-text="item.name"></div> <div class="pb-2 font-bold text-white" x-text="item.name"></div>
<template x-if="item.status.startsWith('running')"> <template x-if="item.status.startsWith('running')">
@@ -227,7 +227,7 @@
<div title="degraded" class="mt-1 bg-warning badge badge-xs"></div> <div title="degraded" class="mt-1 bg-warning badge badge-xs"></div>
</template> </template>
</div> </div>
<div class="description" x-text="item.description"></div> <div class="max-w-full truncate description" x-text="item.description"></div>
</div> </div>
</a> </a>
<div class="flex gap-1 pt-1 group-hover:text-white group min-h-6"> <div class="flex gap-1 pt-1 group-hover:text-white group min-h-6">
@@ -243,7 +243,7 @@
<template x-for="item in filteredServices" :key="item.id"> <template x-for="item in filteredServices" :key="item.id">
<span> <span>
<a class="h-24 box group" :href="item.hrefLink"> <a class="h-24 box group" :href="item.hrefLink">
<div class="flex flex-col mx-6"> <div class="flex flex-col px-4 mx-2">
<div class="flex gap-2"> <div class="flex gap-2">
<div class="pb-2 font-bold text-white" x-text="item.name"></div> <div class="pb-2 font-bold text-white" x-text="item.name"></div>
<template x-if="item.status.startsWith('running')"> <template x-if="item.status.startsWith('running')">
@@ -259,7 +259,7 @@
<div title="degraded" class="mt-1 bg-warning badge badge-xs"></div> <div title="degraded" class="mt-1 bg-warning badge badge-xs"></div>
</template> </template>
</div> </div>
<div class="description" x-text="item.description"></div> <div class="max-w-full truncate description" x-text="item.description"></div>
</div> </div>
</a> </a>
<div class="flex gap-1 pt-1 group-hover:text-white group min-h-6"> <div class="flex gap-1 pt-1 group-hover:text-white group min-h-6">

View File

@@ -1,4 +1,4 @@
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'service-stack' }" x-init="$wire.checkStatus"> <div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'service-stack' }" x-init="$wire.check_status" wire:poll.5000ms="check_status">
<livewire:project.service.navbar :service="$service" :parameters="$parameters" :query="$query" /> <livewire:project.service.navbar :service="$service" :parameters="$parameters" :query="$query" />
<div class="flex h-full pt-6"> <div class="flex h-full pt-6">
<div class="flex flex-col items-start gap-4 min-w-fit"> <div class="flex flex-col items-start gap-4 min-w-fit">

View File

@@ -12,23 +12,44 @@
@elseif ($type === 'service') @elseif ($type === 'service')
<h2>Execute Command</h2> <h2>Execute Command</h2>
@endif @endif
@if (count($containers) > 0) <div x-init="$wire.loadContainers">
<form class="flex flex-col gap-2 pt-4" wire:submit='runCommand'> <div class="pt-4" wire:loading wire:target='loadContainers'>
<div class="flex gap-2"> Loading containers...
<x-forms.input placeholder="ls -l" autofocus id="command" label="Command" required /> </div>
<x-forms.input id="workDir" label="Working directory" /> <div wire:loading.remove wire:target='loadContainers'>
</div> @if (count($containers) > 0)
<x-forms.select label="Container" id="container" required> <form class="flex flex-col gap-2 pt-4" wire:submit='runCommand'>
<option disabled selected>Select container</option> <div class="flex gap-2">
@foreach ($containers as $container) <x-forms.input placeholder="ls -l" autofocus id="command" label="Command" required />
<option value="{{ $container }}">{{ $container }}</option> <x-forms.input id="workDir" label="Working directory" />
@endforeach </div>
</x-forms.select> <x-forms.select label="Container" id="container" required>
<x-forms.button type="submit">Run</x-forms.button> <option disabled selected>Select container</option>
</form> @if (data_get($this->parameters, 'application_uuid'))
@else @foreach ($containers as $container)
<div class="pt-4">No containers are not running.</div> <option value="{{ data_get($container, 'container.Names') }}">
@endif {{ data_get($container, 'container.Names') }}
</option>
@endforeach
@elseif(data_get($this->parameters, 'service_uuid'))
@foreach ($containers as $container)
<option value="{{ $container }}">
{{ $container }}
</option>
@endforeach
@else
<option value="{{ $container }}">
{{ $container }}
</option>
@endif
</x-forms.select>
<x-forms.button type="submit">Run</x-forms.button>
</form>
@else
<div class="pt-4">No containers are not running.</div>
@endif
</div>
</div>
<div class="container w-full pt-10 mx-auto"> <div class="container w-full pt-10 mx-auto">
<livewire:activity-monitor header="Command output" /> <livewire:activity-monitor header="Command output" />
</div> </div>

View File

@@ -1,23 +1,31 @@
<div> <div>
<div x-init="$wire.getLogs"> <div x-init="$wire.getLogs" id="screen" x-data="{ fullscreen: false, alwaysScroll: false, intervalId: null }">
<div class="flex gap-2"> <div class="flex items-center gap-2">
<h4>Container: {{ $container }}</h4> @if ($resource->type() === 'application')
<h3>{{ $container }}</h3>
@else
<h3>{{ str($container)->beforeLast('-')->headline() }}</h3>
@endif
<div>Server: {{ $server->name }} </div>
@if ($pull_request)
<div>({{ $pull_request }})</div>
@endif
@if ($streamLogs) @if ($streamLogs)
<span wire:poll.2000ms='getLogs(true)' class="loading loading-xs text-warning loading-spinner"></span> <span wire:poll.2000ms='getLogs(true)' class="loading loading-xs text-warning loading-spinner"></span>
@endif @endif
</div> </div>
<div class="flex gap-2"> <form wire:submit='getLogs(true)' class="flex items-end gap-2 pt-2 ">
<div class="w-96">
<x-forms.input label="Only Show Number of Lines" placeholder="1000" required
id="numberOfLines"></x-forms.input>
</div>
<x-forms.button type="submit">Refresh</x-forms.button>
<x-forms.checkbox instantSave label="Stream Logs" id="streamLogs"></x-forms.checkbox> <x-forms.checkbox instantSave label="Stream Logs" id="streamLogs"></x-forms.checkbox>
<x-forms.checkbox instantSave label="Include Timestamps" id="showTimeStamps"></x-forms.checkbox> <x-forms.checkbox instantSave label="Include Timestamps" id="showTimeStamps"></x-forms.checkbox>
</div>
<form wire:submit='getLogs(true)' class="flex items-end gap-2">
<x-forms.input label="Only Show Number of Lines" placeholder="1000" required
id="numberOfLines"></x-forms.input>
<x-forms.button type="submit">Refresh</x-forms.button>
</form> </form>
<div id="screen" x-data="{ fullscreen: false, alwaysScroll: false, intervalId: null }" :class="fullscreen ? 'fullscreen' : 'w-full py-4 mx-auto'"> <div :class="fullscreen ? 'fullscreen' : 'relative w-full py-4 mx-auto'">
<div class="relative flex flex-col-reverse w-full p-4 pt-6 overflow-y-auto text-white bg-coolgray-100 scrollbar border-coolgray-300" <div class="flex flex-col-reverse w-full px-4 py-2 overflow-y-auto text-white bg-coolgray-100 scrollbar border-coolgray-300"
:class="fullscreen ? '' : 'max-h-[40rem] border border-solid rounded'"> :class="fullscreen ? '' : 'max-h-96 border border-solid rounded'">
<button title="Minimize" x-show="fullscreen" class="fixed top-4 right-4" <button title="Minimize" x-show="fullscreen" class="fixed top-4 right-4"
x-on:click="makeFullscreen"><svg class="icon" viewBox="0 0 24 24" x-on:click="makeFullscreen"><svg class="icon" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"> xmlns="http://www.w3.org/2000/svg">
@@ -36,8 +44,8 @@
stroke-width="2" d="M12 5v14m4-4l-4 4m-4-4l4 4" /> stroke-width="2" d="M12 5v14m4-4l-4 4m-4-4l4 4" />
</svg></button> </svg></button>
<button title="Fullscreen" x-show="!fullscreen" class="absolute top-2 right-2" <button title="Fullscreen" x-show="!fullscreen" class="absolute top-6 right-4"
x-on:click="makeFullscreen"><svg class=" icon" viewBox="0 0 24 24" x-on:click="makeFullscreen"><svg class="icon" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"> xmlns="http://www.w3.org/2000/svg">
<g fill="none"> <g fill="none">
<path <path
@@ -46,7 +54,11 @@
d="M9.793 12.793a1 1 0 0 1 1.497 1.32l-.083.094L6.414 19H9a1 1 0 0 1 .117 1.993L9 21H4a1 1 0 0 1-.993-.883L3 20v-5a1 1 0 0 1 1.993-.117L5 15v2.586l4.793-4.793ZM20 3a1 1 0 0 1 .993.883L21 4v5a1 1 0 0 1-1.993.117L19 9V6.414l-4.793 4.793a1 1 0 0 1-1.497-1.32l.083-.094L17.586 5H15a1 1 0 0 1-.117-1.993L15 3h5Z" /> d="M9.793 12.793a1 1 0 0 1 1.497 1.32l-.083.094L6.414 19H9a1 1 0 0 1 .117 1.993L9 21H4a1 1 0 0 1-.993-.883L3 20v-5a1 1 0 0 1 1.993-.117L5 15v2.586l4.793-4.793ZM20 3a1 1 0 0 1 .993.883L21 4v5a1 1 0 0 1-1.993.117L19 9V6.414l-4.793 4.793a1 1 0 0 1-1.497-1.32l.083-.094L17.586 5H15a1 1 0 0 1-.117-1.993L15 3h5Z" />
</g> </g>
</svg></button> </svg></button>
<pre id="logs" class="font-mono whitespace-pre-wrap">{{ $outputs }}</pre> @if ($outputs)
<pre id="logs" class="font-mono whitespace-pre-wrap">{{ $outputs }}</pre>
@else
<pre id="logs" class="font-mono whitespace-pre-wrap">Refresh to get the logs...</pre>
@endif
</div> </div>
</div> </div>
<script> <script>

View File

@@ -3,14 +3,20 @@
<h1>Logs</h1> <h1>Logs</h1>
<livewire:project.application.heading :application="$resource" /> <livewire:project.application.heading :application="$resource" />
<div class="pt-4"> <div class="pt-4">
@forelse ($containers as $container) <h2 class="pb-4">Logs</h2>
@if ($loop->first) <div class="pt-2" wire:loading wire:target="loadContainers">
<h2 class="pb-4">Logs</h2> Loading containers...
@endif </div>
<livewire:project.shared.get-logs :server="$server" :resource="$resource" :container="$container" /> @foreach ($servers as $server)
@empty <h3 x-init="$wire.loadContainers({{ $server->id }})"></h3>
<div>No containers are not running.</div> <div wire:loading.remove wire:target="loadContainers">
@endforelse @forelse (data_get($server,'containers',[]) as $container)
<livewire:project.shared.get-logs :server="$server" :resource="$resource" :container="data_get($container,'Names')" />
@empty
<div class="pt-2">No containers are not running on server: {{$server->name}}</div>
@endforelse
</div>
@endforeach
</div> </div>
@elseif ($type === 'database') @elseif ($type === 'database')
<h1>Logs</h1> <h1>Logs</h1>
@@ -20,9 +26,9 @@
@if ($loop->first) @if ($loop->first)
<h2 class="pb-4">Logs</h2> <h2 class="pb-4">Logs</h2>
@endif @endif
<livewire:project.shared.get-logs :server="$server" :resource="$resource" :container="$container" /> <livewire:project.shared.get-logs :server="$servers[0]" :resource="$resource" :container="$container" />
@empty @empty
<div>No containers are not running.</div> <div class="pt-2">No containers are not running.</div>
@endforelse @endforelse
</div> </div>
@elseif ($type === 'service') @elseif ($type === 'service')
@@ -31,9 +37,9 @@
@if ($loop->first) @if ($loop->first)
<h2 class="pb-4">Logs</h2> <h2 class="pb-4">Logs</h2>
@endif @endif
<livewire:project.shared.get-logs :server="$server" :resource="$resource" :container="$container" /> <livewire:project.shared.get-logs :server="$servers[0]" :resource="$resource" :container="$container" />
@empty @empty
<div>No containers are not running.</div> <div class="pt-2">No containers are not running.</div>
@endforelse @endforelse
</div> </div>
@endif @endif

View File

@@ -11,7 +11,7 @@
</svg> </svg>
</div> </div>
@empty @empty
<div>No tags yet</div> <div class="py-1">No tags yet</div>
@endforelse @endforelse
</div> </div>
<form wire:submit='submit' class="flex items-end gap-2 pt-4"> <form wire:submit='submit' class="flex items-end gap-2 pt-4">
@@ -23,8 +23,7 @@
<x-forms.button type="submit">Add</x-forms.button> <x-forms.button type="submit">Add</x-forms.button>
</form> </form>
@if ($tags->count() > 0) @if ($tags->count() > 0)
<h3 class="pt-4">Already defined tags</h3> <h3 class="pt-4">Quickly Add</h3>
<div>Click to quickly add one.</div>
<div class="flex gap-2 pt-4"> <div class="flex gap-2 pt-4">
@foreach ($tags as $tag) @foreach ($tags as $tag)
<x-forms.button wire:click="addTag('{{ $tag->id }}','{{ $tag->name }}')"> <x-forms.button wire:click="addTag('{{ $tag->id }}','{{ $tag->name }}')">

View File

@@ -7,10 +7,10 @@
back! back!
</div> </div>
@if ($server->definedResources()->count() > 0) @if ($server->definedResources()->count() > 0)
<div class="pb-2 text-red-500">You need to delete all resources before deleting this server.</div>
<x-new-modal disabled isErrorButton buttonTitle="Delete"> <x-new-modal disabled isErrorButton buttonTitle="Delete">
This server will be deleted. It is not reversible. <br>Please think again. This server will be deleted. It is not reversible. <br>Please think again.
</x-new-modal> </x-new-modal>
<div>You need to delete all resources before deleting this server.</div>
@else @else
<x-new-modal isErrorButton buttonTitle="Delete"> <x-new-modal isErrorButton buttonTitle="Delete">
This server will be deleted. It is not reversible. <br>Please think again. This server will be deleted. It is not reversible. <br>Please think again.

View File

@@ -4,7 +4,7 @@
<h2>General</h2> <h2>General</h2>
@if ($server->id === 0) @if ($server->id === 0)
<x-new-modal buttonTitle="Save" title="Change Localhost" action="submit"> <x-new-modal buttonTitle="Save" title="Change Localhost" action="submit">
You could lost a lot of functionalities if you change the server details of the server where Coolify You could lose a lot of functionalities if you change the server details of the server where Coolify
is is
running on.<br>Please think again. running on.<br>Please think again.
</x-new-modal> </x-new-modal>
@@ -47,6 +47,10 @@
Validate Server Validate Server
</x-forms.button> </x-forms.button>
@endif @endif
@if ($server->isForceDisabled() && isCloud())
<div class="pt-4 font-bold text-red-500">The system has disabled the server because you have exceeded the
number of servers for which you have paid.</div>
@endif
<div class="flex flex-col gap-2 pt-4"> <div class="flex flex-col gap-2 pt-4">
<div class="flex flex-col w-full gap-2 lg:flex-row"> <div class="flex flex-col w-full gap-2 lg:flex-row">
<x-forms.input id="server.name" label="Name" required /> <x-forms.input id="server.name" label="Name" required />

View File

@@ -1,18 +1,18 @@
<div> <div>
<div class="flex items-start gap-2"> <div class="flex items-start gap-2">
<h1>Servers</h1> <h1>Servers</h1>
<a class="text-white hover:no-underline" href="{{ route('server.create') }}"> <a class="text-white hover:no-underline" href="{{ route('server.create') }}">
<x-forms.button class="btn">+ Add</x-forms.button> <x-forms.button class="btn">+ Add</x-forms.button>
</a> </a>
</div> </div>
<div class="subtitle ">All Servers</div> <div class="subtitle ">All Servers</div>
<div class="grid gap-2 lg:grid-cols-2"> <div class="grid gap-2 lg:grid-cols-2">
@forelse ($servers as $server) @forelse ($servers as $server)
<a href="{{ route('server.show', ['server_uuid' => data_get($server, 'uuid')]) }}" <a href="{{ route('server.show', ['server_uuid' => data_get($server, 'uuid')]) }}"
@class([ @class([
'gap-2 border cursor-pointer box group', 'gap-2 border cursor-pointer box group',
'border-transparent' => $server->settings->is_reachable, 'border-transparent' => $server->settings->is_reachable && $server->settings->is_usable && !$server->settings->force_disabled,
'border-red-500' => !$server->settings->is_reachable, 'border-red-500' => !$server->settings->is_reachable || $server->settings->force_disabled,
])> ])>
<div class="flex flex-col mx-6"> <div class="flex flex-col mx-6">
<div class="font-bold text-white"> <div class="font-bold text-white">
@@ -30,6 +30,9 @@
@if (!$server->settings->is_usable) @if (!$server->settings->is_usable)
<span>Not usable by Coolify</span> <span>Not usable by Coolify</span>
@endif @endif
@if ($server->settings->force_disabled)
<span>Disabled by the system</span>
@endif
</div> </div>
</div> </div>
<div class="flex-1"></div> <div class="flex-1"></div>

View File

@@ -48,7 +48,6 @@
<div wire:loading.remove> No dynamic configurations found.</div> <div wire:loading.remove> No dynamic configurations found.</div>
@endif @endif
</div> </div>
@endif @endif
</div> </div>
</div> </div>

View File

@@ -1,11 +1,13 @@
<div> <div>
<x-server.navbar :server="$server" :parameters="$parameters" /> <x-server.navbar :server="$server" :parameters="$parameters" />
<div class="flex gap-2"> @if ($server->isFunctional())
<x-server.sidebar :server="$server" :parameters="$parameters" /> <div class="flex gap-2">
<div class="w-full"> <x-server.sidebar :server="$server" :parameters="$parameters" />
@if ($server->isFunctional()) <div class="w-full">
<livewire:server.proxy :server="$server" /> <livewire:server.proxy :server="$server" />
@endif </div>
</div> </div>
</div> @else
<div>Server is not validated. Validate first.</div>
@endif
</div> </div>

View File

@@ -45,7 +45,7 @@
{{ data_get($resource, 'environment.name') }} {{ data_get($resource, 'environment.name') }}
</td> </td>
<td class="px-5 py-4 text-sm whitespace-nowrap"><a class="" <td class="px-5 py-4 text-sm whitespace-nowrap"><a class=""
href="{{ $resource->link() }}">{{ $resource->name }} </a> href="{{ $resource->link() }}">{{ $resource->name }} <x-internal-link/></a>
</td> </td>
<td class="px-5 py-4 text-sm whitespace-nowrap"> <td class="px-5 py-4 text-sm whitespace-nowrap">
{{ str($resource->type())->headline() }}</td> {{ str($resource->type())->headline() }}</td>
@@ -68,7 +68,7 @@
</div> </div>
</div> </div>
<div x-cloak x-show="activeTab === 'unmanaged'" class="h-full"> <div x-cloak x-show="activeTab === 'unmanaged'" class="h-full">
<div class="flex flex-col"> <div class="flex flex-col" x-init="$wire.loadUnmanagedContainers()">
<div class="flex gap-2"> <div class="flex gap-2">
<h2>Resources</h2> <h2>Resources</h2>
<x-forms.button wire:click="refreshStatus">Refresh</x-forms.button> <x-forms.button wire:click="refreshStatus">Refresh</x-forms.button>

View File

@@ -1,7 +1,8 @@
<div> <div>
@if (subscriptionProvider() === 'stripe') @if (subscriptionProvider() === 'stripe')
<div class="pt-4"> <div class="pt-4">
<div class="pb-4">Your current Plan is: <strong <h2>Your current plan</h2>
<div class="pb-4">Tier: <strong
class="text-warning">{{ data_get(currentTeam(), 'subscription')->type() }}</strong></div> class="text-warning">{{ data_get(currentTeam(), 'subscription')->type() }}</strong></div>
@if (currentTeam()->subscription->stripe_cancel_at_period_end) @if (currentTeam()->subscription->stripe_cancel_at_period_end)
@@ -10,13 +11,19 @@
<div>Subscription is active. Last invoice is <div>Subscription is active. Last invoice is
{{ currentTeam()->subscription->stripe_invoice_paid ? 'paid' : 'not paid' }}.</div> {{ currentTeam()->subscription->stripe_invoice_paid ? 'paid' : 'not paid' }}.</div>
@endif @endif
<h3 class="pt-4">Limits</h3> <div>Number of paid servers: {{ $server_limits }}</div>
<div>Server: {{ $server_limits }}</div> <div>Currently active servers: {{ currentTeam()->servers->count() }}</div>
<h3 class="pt-4">Actions</h3> @if (currentTeam()->serverOverflow())
<div class="py-4"><span class="font-bold text-red-500">WARNING:</span> You must delete
{{ currentTeam()->servers->count() - $server_limits }} servers,
or upgrade your subscription. {{ currentTeam()->servers->count() - $server_limits }} servers will be
deactivated.</div>
@endif
<h2 class="pt-4">Manage your subscription</h2>
<div class="pb-4">Cancel, upgrade or downgrade your subscription.</div> <div class="pb-4">Cancel, upgrade or downgrade your subscription.</div>
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.button wire:click='stripeCustomerPortal'>Manage your subscription on <svg <x-forms.button wire:click='stripeCustomerPortal'>Go to <svg xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" class="w-12" viewBox="0 0 512 214"> class="w-12" viewBox="0 0 512 214">
<path fill="#635BFF" <path fill="#635BFF"
d="M512 110.08c0-36.409-17.636-65.138-51.342-65.138c-33.85 0-54.33 28.73-54.33 64.854c0 42.808 24.179 64.426 58.88 64.426c16.925 0 29.725-3.84 39.396-9.244v-28.445c-9.67 4.836-20.764 7.823-34.844 7.823c-13.796 0-26.027-4.836-27.591-21.618h69.547c0-1.85.284-9.245.284-12.658Zm-70.258-13.511c0-16.071 9.814-22.756 18.774-22.756c8.675 0 17.92 6.685 17.92 22.756h-36.694Zm-90.31-51.627c-13.939 0-22.899 6.542-27.876 11.094l-1.85-8.818h-31.288v165.83l35.555-7.537l.143-40.249c5.12 3.698 12.657 8.96 25.173 8.96c25.458 0 48.64-20.48 48.64-65.564c-.142-41.245-23.609-63.716-48.498-63.716Zm-8.534 97.991c-8.391 0-13.37-2.986-16.782-6.684l-.143-52.765c3.698-4.124 8.818-6.968 16.925-6.968c12.942 0 21.902 14.506 21.902 33.137c0 19.058-8.818 33.28-21.902 33.28ZM241.493 36.551l35.698-7.68V0l-35.698 7.538V36.55Zm0 10.809h35.698v124.444h-35.698V47.36Zm-38.257 10.524L200.96 47.36h-30.72v124.444h35.556V87.467c8.39-10.951 22.613-8.96 27.022-7.396V47.36c-4.551-1.707-21.191-4.836-29.582 10.524Zm-71.112-41.386l-34.702 7.395l-.142 113.92c0 21.05 15.787 36.551 36.836 36.551c11.662 0 20.195-2.133 24.888-4.693V140.8c-4.55 1.849-27.022 8.391-27.022-12.658V77.653h27.022V47.36h-27.022l.142-30.862ZM35.982 83.484c0-5.546 4.551-7.68 12.09-7.68c10.808 0 24.461 3.272 35.27 9.103V51.484c-11.804-4.693-23.466-6.542-35.27-6.542C19.2 44.942 0 60.018 0 85.192c0 39.252 54.044 32.995 54.044 49.92c0 6.541-5.688 8.675-13.653 8.675c-11.804 0-26.88-4.836-38.827-11.378v33.849c13.227 5.689 26.596 8.106 38.827 8.106c29.582 0 49.92-14.648 49.92-40.106c-.142-42.382-54.329-34.845-54.329-50.774Z" /> d="M512 110.08c0-36.409-17.636-65.138-51.342-65.138c-33.85 0-54.33 28.73-54.33 64.854c0 42.808 24.179 64.426 58.88 64.426c16.925 0 29.725-3.84 39.396-9.244v-28.445c-9.67 4.836-20.764 7.823-34.844 7.823c-13.796 0-26.027-4.836-27.591-21.618h69.547c0-1.85.284-9.245.284-12.658Zm-70.258-13.511c0-16.071 9.814-22.756 18.774-22.756c8.675 0 17.92 6.685 17.92 22.756h-36.694Zm-90.31-51.627c-13.939 0-22.899 6.542-27.876 11.094l-1.85-8.818h-31.288v165.83l35.555-7.537l.143-40.249c5.12 3.698 12.657 8.96 25.173 8.96c25.458 0 48.64-20.48 48.64-65.564c-.142-41.245-23.609-63.716-48.498-63.716Zm-8.534 97.991c-8.391 0-13.37-2.986-16.782-6.684l-.143-52.765c3.698-4.124 8.818-6.968 16.925-6.968c12.942 0 21.902 14.506 21.902 33.137c0 19.058-8.818 33.28-21.902 33.28ZM241.493 36.551l35.698-7.68V0l-35.698 7.538V36.55Zm0 10.809h35.698v124.444h-35.698V47.36Zm-38.257 10.524L200.96 47.36h-30.72v124.444h35.556V87.467c8.39-10.951 22.613-8.96 27.022-7.396V47.36c-4.551-1.707-21.191-4.836-29.582 10.524Zm-71.112-41.386l-34.702 7.395l-.142 113.92c0 21.05 15.787 36.551 36.836 36.551c11.662 0 20.195-2.133 24.888-4.693V140.8c-4.55 1.849-27.022 8.391-27.022-12.658V77.653h27.022V47.36h-27.022l.142-30.862ZM35.982 83.484c0-5.546 4.551-7.68 12.09-7.68c10.808 0 24.461 3.272 35.27 9.103V51.484c-11.804-4.693-23.466-6.542-35.27-6.542C19.2 44.942 0 60.018 0 85.192c0 39.252 54.044 32.995 54.044 49.92c0 6.541-5.688 8.675-13.653 8.675c-11.804 0-26.88-4.836-38.827-11.378v33.849c13.227 5.689 26.596 8.106 38.827 8.106c29.582 0 49.92-14.648 49.92-40.106c-.142-42.382-54.329-34.845-54.329-50.774Z" />
</svg></x-forms.button> </svg></x-forms.button>

View File

@@ -22,16 +22,13 @@
</x-forms.button> </x-forms.button>
</x-slot:pro> </x-slot:pro>
<x-slot:ultimate> <x-slot:ultimate>
<x-forms.button x-show="selected === 'monthly'" x-cloak aria-describedby="tier-ultimate" <x-forms.button x-show="selected === 'monthly'" x-cloak aria-describedby="tier-ultimate" class="w-full h-10 buyme"
class="w-full h-10 buyme"><a class="text-white hover:no-underline" href="{{ config('coolify.contact') }}" wire:click="subscribeStripe('ultimate-monthly')">
target="_blank"> {{ $isTrial ? 'Start Trial' : 'Subscribe' }}
Contact Us</a>
</x-forms.button> </x-forms.button>
<x-forms.button x-show="selected === 'yearly'" x-cloak aria-describedby="tier-ultimate" <x-forms.button x-show="selected === 'yearly'" x-cloak aria-describedby="tier-ultimate" class="w-full h-10 buyme"
class="w-full h-10 buyme"><a class="text-white hover:no-underline" href="{{ config('coolify.contact') }}" wire:click="subscribeStripe('ultimate-yearly')"> {{ $isTrial ? 'Start Trial' : 'Subscribe' }}
target="_blank">
Contact Us</a>
</x-forms.button> </x-forms.button>
</x-slot:ultimate> </x-slot:ultimate>
@endif @endif

View File

@@ -1,6 +1,5 @@
<?php <?php
use App\Http\Controllers\Api\Server as ApiServer;
use App\Models\GitlabApp; use App\Models\GitlabApp;
use App\Models\PrivateKey; use App\Models\PrivateKey;
use App\Models\Server; use App\Models\Server;

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@ set -e # Exit immediately if a command exits with a non-zero status
#set -u # Treat unset variables as an error and exit #set -u # Treat unset variables as an error and exit
set -o pipefail # Cause a pipeline to return the status of the last command that exited with a non-zero status set -o pipefail # Cause a pipeline to return the status of the last command that exited with a non-zero status
VERSION="1.2.0" VERSION="1.2.1"
DOCKER_VERSION="24.0" DOCKER_VERSION="24.0"
CDN="https://cdn.coollabs.io/coolify" CDN="https://cdn.coollabs.io/coolify"
@@ -27,11 +27,11 @@ if [ $EUID != 0 ]; then
fi fi
case "$OS_TYPE" in case "$OS_TYPE" in
arch | ubuntu | debian | raspbian | centos | fedora | rhel | ol | rocky | sles | opensuse-leap | opensuse-tumbleweed) ;; arch | ubuntu | debian | raspbian | centos | fedora | rhel | ol | rocky | sles | opensuse-leap | opensuse-tumbleweed) ;;
*) *)
echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now." echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now."
exit exit
;; ;;
esac esac
# Overwrite LATEST_VERSION if user pass a version number # Overwrite LATEST_VERSION if user pass a version number
@@ -54,27 +54,27 @@ echo -e "-------------"
echo "Installing required packages..." echo "Installing required packages..."
case "$OS_TYPE" in case "$OS_TYPE" in
arch) arch)
pacman -Sy >/dev/null 2>&1 || true pacman -Sy >/dev/null 2>&1 || true
if ! pacman -Q curl wget git jq >/dev/null 2>&1; then if ! pacman -Q curl wget git jq >/dev/null 2>&1; then
pacman -S --noconfirm curl wget git jq >/dev/null 2>&1 || true pacman -S --noconfirm curl wget git jq >/dev/null 2>&1 || true
fi fi
;; ;;
ubuntu | debian | raspbian) ubuntu | debian | raspbian)
apt update -y >/dev/null 2>&1 apt update -y >/dev/null 2>&1
apt install -y curl wget git jq >/dev/null 2>&1 apt install -y curl wget git jq >/dev/null 2>&1
;; ;;
centos | fedora | rhel | ol | rocky) centos | fedora | rhel | ol | rocky)
dnf install -y curl wget git jq >/dev/null 2>&1 dnf install -y curl wget git jq >/dev/null 2>&1
;; ;;
sles | opensuse-leap | opensuse-tumbleweed) sles | opensuse-leap | opensuse-tumbleweed)
zypper refresh >/dev/null 2>&1 zypper refresh >/dev/null 2>&1
zypper install -y curl wget git jq >/dev/null 2>&1 zypper install -y curl wget git jq >/dev/null 2>&1
;; ;;
*) *)
echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now." echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now."
exit exit
;; ;;
esac esac
# Detect OpenSSH server # Detect OpenSSH server
@@ -113,7 +113,6 @@ if [ "$SSH_PERMIT_ROOT_LOGIN_CONFIG" = "prohibit-password" ] || [ "$SSH_PERMIT_R
SSH_PERMIT_ROOT_LOGIN=true SSH_PERMIT_ROOT_LOGIN=true
fi fi
if [ "$SSH_PERMIT_ROOT_LOGIN" != "true" ]; then if [ "$SSH_PERMIT_ROOT_LOGIN" != "true" ]; then
echo "###############################################################################" echo "###############################################################################"
echo "WARNING: PermitRootLogin is not enabled in /etc/ssh/sshd_config." echo "WARNING: PermitRootLogin is not enabled in /etc/ssh/sshd_config."
@@ -198,7 +197,7 @@ fi
echo -e "-------------" echo -e "-------------"
mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy} mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,webhooks-during-maintenance}
mkdir -p /data/coolify/ssh/{keys,mux} mkdir -p /data/coolify/ssh/{keys,mux}
mkdir -p /data/coolify/proxy/dynamic mkdir -p /data/coolify/proxy/dynamic

View File

@@ -1,5 +1,5 @@
#!/bin/bash #!/bin/bash
## Do not modify this file. You will lost the ability to autoupdate! ## Do not modify this file. You will lose the ability to autoupdate!
VERSION="1.0.4" VERSION="1.0.4"
CDN="https://cdn.coollabs.io/coolify" CDN="https://cdn.coollabs.io/coolify"

View File

@@ -16,6 +16,7 @@ services:
- DB_USERNAME=$SERVICE_USER_MYSQL - DB_USERNAME=$SERVICE_USER_MYSQL
- DB_PASSWORD=$SERVICE_PASSWORD_MYSQL - DB_PASSWORD=$SERVICE_PASSWORD_MYSQL
- STATIC_CRON_TOKEN=$SERVICE_BASE64_CRONTOKEN - STATIC_CRON_TOKEN=$SERVICE_BASE64_CRONTOKEN
- TRUSTED_PROXIES=*
volumes: volumes:
- firefly-upload:/var/www/html/storage/upload - firefly-upload:/var/www/html/storage/upload
healthcheck: healthcheck:
@@ -49,8 +50,20 @@ services:
retries: 10 retries: 10
volumes: volumes:
- firefly-mysql-data:/var/lib/mysql - firefly-mysql-data:/var/lib/mysql
# cron: cron:
# image: alpine image: alpine
# command: sh -c "echo \"0 3 * * * wget -qO- http://app:8080/api/v1/cron/$STATIC_CRON_TOKEN\" | crontab - && crond -f -L /dev/stdout" entrypoint: ["/entrypoint.sh"]
# environment: volumes:
# - STATIC_CRON_TOKEN=$SERVICE_PASSWORD_32_CRONTOKEN - type: bind
source: ./entrypoint.sh
target: /entrypoint.sh
content: |
#!/bin/sh
# Substitute the environment variable into the cron command
CRON_COMMAND="0 3 * * * wget -qO- http://firefly:8080/api/v1/cron/${STATIC_CRON_TOKEN}"
# Add the cron command to the crontab
echo "$CRON_COMMAND" | crontab -
# Start the cron daemon in the foreground with logging to stdout
crond -f -L /dev/stdout
environment:
- STATIC_CRON_TOKEN=$SERVICE_BASE64_CRONTOKEN

View File

@@ -0,0 +1,100 @@
# ignore: true
# documentation: https://invoiceninja.github.io/selfhost.html
# slogan: The leading open-source invoicing platform
# tags: invoicing, billing, accounting, finance, self-hosted
services:
invoice-ninja:
image: invoiceninja/invoiceninja:5
environment:
- SERVICE_FQDN_INVOICENINJA
- APP_ENV=production
- APP_URL=${SERVICE_FQDN_INVOICENINJA}
- APP_KEY=${SERVICE_BASE64_INVOICENINJA}
- APP_DEBUG=false
- REQUIRE_HTTPS=false
- PHANTOMJS_PDF_GENERATION=false
- PDF_GENERATOR=snappdf
- TRUSTED_PROXIES=*
- QUEUE_CONNECTION=database
- DB_HOST=mysql
- DB_PORT=3306
- DB_DATABASE=${MYSQL_DATABASE:-invoice_ninja}
- DB_USERNAME=${SERVICE_USER_MYSQL}
- DB_PASSWORD=${SERVICE_PASSWORD_MYSQL}
volumes:
- invoice-ninja-public:/var/www/app/public
- invoice-ninja-storage:/var/www/app/storage
- type: bind
source: ./php.ini
target: /usr/local/etc/php/php.ini
content: |
session.auto_start = Off
short_open_tag = Off
error_reporting = E_ALL & ~E_NOTICE & ~E_WARNING & ~E_STRICT & ~E_DEPRECATED
; opcache.enable=1
; opcache.preload=/srv/www/invoiceninja/current/preload.php
; opcache.preload_user=www-data
; ; The OPcache shared memory storage size.
; opcache.max_accelerated_files=300000
; opcache.validate_timestamps=1
; opcache.revalidate_freq=30
; opcache.jit_buffer_size=256M
; opcache.jit=1205
; opcache.memory_consumption=1024M
post_max_size = 60M
upload_max_filesize = 50M
memory_limit=512M
- type: bind
source: ./php-cli.ini
target: /usr/local/etc/php/php-cli.ini
content: |
session.auto_start = Off
short_open_tag = Off
error_reporting = E_ALL & ~E_NOTICE & ~E_WARNING & ~E_STRICT & ~E_DEPRECATED
; opcache.enable_cli=1
; opcache.fast_shutdown=1
; opcache.memory_consumption=256
; opcache.interned_strings_buffer=8
; opcache.max_accelerated_files=4000
; opcache.revalidate_freq=60
; # http://symfony.com/doc/current/performance.html
; realpath_cache_size = 4096K
; realpath_cache_ttl = 600
memory_limit = 2G
post_max_size = 60M
upload_max_filesize = 50M
depends_on:
mysql:
condition: service_healthy
mysql:
image: mariadb:lts
environment:
- MYSQL_USER=${SERVICE_USER_MYSQL}
- MYSQL_PASSWORD=${SERVICE_PASSWORD_MYSQL}
- MYSQL_DATABASE=${MYSQL_DATABASE:-invoice_ninja}
- MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_MYSQLROOT}
healthcheck:
test:
[
"CMD",
"mysqladmin",
"ping",
"-h",
"localhost",
"-uroot",
"-p${SERVICE_PASSWORD_MYSQLROOT}",
]
interval: 5s
timeout: 20s
retries: 10
volumes:
- invoice-ninja-mysql-data:/var/lib/mysql

Some files were not shown because too many files have changed in this diff Show More