Compare commits

...

74 Commits

Author SHA1 Message Date
Andras Bacsai
3dd00dd91a Merge pull request #1811 from coollabsio/next
v4.0.0-beta.235
2024-03-05 10:29:41 +01:00
Andras Bacsai
fd97c5085b Update devDependencies in package.json 2024-03-05 10:26:32 +01:00
Andras Bacsai
fa6a249fb4 Update Docker installation for AlmaLinux 2024-03-05 10:24:02 +01:00
Andras Bacsai
0aa9b1735b Update currentState in selectExistingServer method 2024-03-05 10:07:28 +01:00
Andras Bacsai
6027bee3b8 Refactor repository selection logic in GithubPrivateRepository.php 2024-03-05 09:58:02 +01:00
Andras Bacsai
4d72787c83 fix: sort repositories by name 2024-03-05 09:40:38 +01:00
Andras Bacsai
c6740cfea0 fix: make sure to show some buttons 2024-03-05 09:37:35 +01:00
Andras Bacsai
9c1d585c43 Fix condition to return current team if user has teams 2024-03-05 09:22:38 +01:00
Andras Bacsai
863acf988e Fix selected repository ID assignment in loadRepositories method 2024-03-05 09:21:12 +01:00
Andras Bacsai
a6b3beafbb Fix issue with loading repositories in GithubPrivateRepository.php 2024-03-05 09:20:50 +01:00
Andras Bacsai
2ffc3f497b fix: should note delete personal teams 2024-03-05 09:19:15 +01:00
Andras Bacsai
f3a279be26 revert delayed jobs 2024-03-05 08:49:11 +01:00
Andras Bacsai
9ad6631747 Update version numbers 2024-03-04 14:32:17 +01:00
Andras Bacsai
0131f5e341 Merge pull request #1807 from coollabsio/next
v4.0.0-beta.234
2024-03-04 13:40:42 +01:00
Andras Bacsai
b5ab9a8da6 Add custom docker run options for application 2024-03-04 13:39:34 +01:00
Andras Bacsai
57fa2709da Add font preloading and DNS prefetching 2024-03-04 13:34:20 +01:00
Andras Bacsai
96c6a198d7 Fix base64 encoding for TOTP_VAULT_KEY 2024-03-04 12:50:56 +01:00
Andras Bacsai
d106d4bd4e Refactor generateEnvValue function to use base64 encoding for certain cases 2024-03-04 12:46:37 +01:00
Andras Bacsai
53cd3091f7 Add Directus service fields to extraFields method 2024-03-04 12:46:33 +01:00
Andras Bacsai
f1e7b870aa Add TOTP_VAULT_KEY environment variable to Plausible service 2024-03-04 12:30:32 +01:00
Andras Bacsai
99fe076b5a Add scheduled task for database cleanup if not in cloud environment 2024-03-04 12:17:33 +01:00
Andras Bacsai
65fcaa17d9 Update exception in PreventRequestsDuringMaintenance middleware and version numbers 2024-03-04 11:41:02 +01:00
Andras Bacsai
89c6563e00 Merge pull request #1806 from coollabsio/next
v4.0.0-beta.233
2024-03-04 11:18:56 +01:00
Andras Bacsai
76b0bef32e Update slogan and logo for changedetection service 2024-03-04 11:17:05 +01:00
Andras Bacsai
c20aa0b256 Refactor method names to use camel case 2024-03-04 11:01:14 +01:00
Andras Bacsai
b4908cfcb4 Merge pull request #1804 from RayBB/change-detection
add changedetection.io template
2024-03-04 10:47:54 +01:00
Andras Bacsai
4fb5b04d27 Update proxy configuration layout 2024-03-04 10:46:53 +01:00
Andras Bacsai
8385bbb0a0 feat: gzip enabled & stipprefix setting
refactor: code
2024-03-04 10:46:13 +01:00
Andras Bacsai
cee6b54033 Add proxy start functionality when selecting a proxy type 2024-03-04 10:42:54 +01:00
Andras Bacsai
0dd591a5ff fix: raw compose make dirs
fix: raw compose add coolify labels
2024-03-04 10:13:40 +01:00
Andras Bacsai
62278126e4 fixes 2024-03-04 09:12:23 +01:00
Andras Bacsai
0aa85a3701 fix: service status updated 2024-03-04 08:57:18 +01:00
Andras Bacsai
0e1ba64836 fix: sentry error 2024-03-04 08:51:24 +01:00
Andras Bacsai
f7e1ce8656 fix: env value generation 2024-03-04 08:49:53 +01:00
RayBB
5030c14dc2 add changedetection.io template 2024-03-03 23:42:33 +01:00
Andras Bacsai
1333cd1d84 Merge pull request #1799 from coollabsio/next
v4.0.0-beta.232
2024-03-02 16:04:06 +01:00
Andras Bacsai
112c259d27 Refactor destinations method in Server model 2024-03-02 15:58:02 +01:00
Andras Bacsai
130d1e1756 Update DockerCleanupJob and version numbers 2024-03-02 15:18:49 +01:00
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
112 changed files with 4274 additions and 1997 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

@@ -15,6 +15,9 @@ class StartProxy
{ {
try { try {
$proxyType = $server->proxyType(); $proxyType = $server->proxyType();
if ($proxyType === 'NONE') {
return 'OK';
}
$commands = collect([]); $commands = collect([]);
$proxy_path = get_proxy_path(); $proxy_path = get_proxy_path();
$configuration = CheckConfiguration::run($server); $configuration = CheckConfiguration::run($server);

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

@@ -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

@@ -47,6 +47,10 @@ class Kernel extends ConsoleKernel
$this->check_resources($schedule); $this->check_resources($schedule);
$this->pull_helper_image($schedule); $this->pull_helper_image($schedule);
$this->check_scheduled_tasks($schedule); $this->check_scheduled_tasks($schedule);
if (!isCloud()) {
$schedule->command('cleanup:database --yes')->daily();
}
} }
} }
private function pull_helper_image($schedule) private function pull_helper_image($schedule)
@@ -69,35 +73,42 @@ 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();
} }
// Delayed Jobs
// foreach ($containerServers as $server) {
// $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()) {
// $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) {
// $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

@@ -77,6 +77,9 @@ class Handler extends ExceptionHandler
); );
} }
); );
if (str($e->getMessage())->contains('No space left on device')) {
return;
}
ray('reporting to sentry'); ray('reporting to sentry');
Integration::captureUnhandledException($e); Integration::captureUnhandledException($e);
}); });

View File

@@ -14,7 +14,7 @@ use Illuminate\Http\Request;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
class APIDeploy extends Controller class Deploy extends Controller
{ {
public function deploy(Request $request) public function deploy(Request $request)
{ {

View File

@@ -6,7 +6,7 @@ use App\Http\Controllers\Controller;
use App\Models\Project as ModelsProject; use App\Models\Project as ModelsProject;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class APIProject extends Controller class Project extends Controller
{ {
public function projects(Request $request) public function projects(Request $request)
{ {

View File

@@ -6,7 +6,7 @@ use App\Http\Controllers\Controller;
use App\Models\Server as ModelsServer; use App\Models\Server as ModelsServer;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class APIServer extends Controller class Server extends Controller
{ {
public function servers(Request $request) public function servers(Request $request)
{ {

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,10 @@ class DecideWhatToDoWithUser
{ {
public function handle(Request $request, Closure $next): Response public function handle(Request $request, Closure $next): Response
{ {
if (auth()?->user()?->teams?->count() === 0) {
$currentTeam = auth()->user()?->recreate_personal_team();
refreshSession($currentTeam);
}
if(auth()?->user()?->currentTeam()){ if(auth()?->user()?->currentTeam()){
refreshSession(auth()->user()->currentTeam()); refreshSession(auth()->user()->currentTeam());
} }

View File

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

View File

@@ -218,12 +218,12 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$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->build_server = $this->server;
$this->original_server = $this->server; $this->original_server = $this->server;
} else { } 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->build_server = $buildServers->random();
$this->application_deployment_queue->addLogEntry("Found a suitable build server ({$this->build_server->name}).");
$this->original_server = $this->server; $this->original_server = $this->server;
$this->use_build_server = true; $this->use_build_server = true;
} }
@@ -374,6 +374,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->cleanup_git(); $this->cleanup_git();
$this->application->loadComposeFile(isInit: false); $this->application->loadComposeFile(isInit: false);
if ($this->application->settings->is_raw_compose_deployment_enabled) { if ($this->application->settings->is_raw_compose_deployment_enabled) {
$this->application->parseRawCompose();
$yaml = $composeFile = $this->application->docker_compose_raw; $yaml = $composeFile = $this->application->docker_compose_raw;
} else { } else {
$composeFile = $this->application->parseCompose(pull_request_id: $this->pull_request_id); $composeFile = $this->application->parseCompose(pull_request_id: $this->pull_request_id);
@@ -413,27 +414,44 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
]); ]);
} }
$this->write_deployment_configurations(); $this->write_deployment_configurations();
// Start compose file // Start compose file
if ($this->docker_compose_custom_start_command) { if ($this->application->settings->is_raw_compose_deployment_enabled) {
$this->execute_remote_command( if ($this->docker_compose_custom_start_command) {
[executeInDocker($this->deployment_uuid, "cd {$this->basedir} && {$this->docker_compose_custom_start_command}"), "hidden" => true], $this->execute_remote_command(
); ["cd {$this->basedir} && {$this->docker_compose_custom_start_command}", "hidden" => true],
);
} else {
$server_workdir = $this->application->workdir();
ray("SOURCE_COMMIT={$this->commit} docker compose --project-directory {$server_workdir} -f {$server_workdir}{$this->docker_compose_location} up -d");
$this->execute_remote_command(
["SOURCE_COMMIT={$this->commit} docker compose --project-directory {$server_workdir} -f {$server_workdir}{$this->docker_compose_location} up -d", "hidden" => true],
);
}
} else { } else {
$this->execute_remote_command( if ($this->docker_compose_custom_start_command) {
[executeInDocker($this->deployment_uuid, "SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up -d"), "hidden" => true], $this->execute_remote_command(
); [executeInDocker($this->deployment_uuid, "cd {$this->basedir} && {$this->docker_compose_custom_start_command}"), "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 -d"), "hidden" => true],
);
}
} }
$this->application_deployment_queue->addLogEntry("New container started."); $this->application_deployment_queue->addLogEntry("New container started.");
} }
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();
@@ -528,9 +546,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(
[ [
@@ -725,7 +745,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.");
@@ -733,6 +753,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 {
@@ -810,26 +834,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()
{ {
@@ -1226,43 +1233,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);
$ipv4 = data_get($custom_compose, 'ip.0'); if (count($custom_compose) > 0) {
$ipv6 = data_get($custom_compose, 'ip6.0'); $ipv4 = data_get($custom_compose, 'ip.0');
data_forget($custom_compose, 'ip'); $ipv6 = data_get($custom_compose, 'ip6.0');
data_forget($custom_compose, 'ip6'); data_forget($custom_compose, 'ip');
if ($ipv4 || $ipv6) { data_forget($custom_compose, 'ip6');
data_forget($docker_compose['services'][$this->application->uuid], 'networks'); if ($ipv4 || $ipv6) {
data_forget($docker_compose['services'][$this->container_name], 'networks');
}
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);
} }
if ($ipv4) { } else {
$docker_compose['services'][$this->application->uuid]['networks'][$this->destination->network]['ipv4_address'] = $ipv4; $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);
} }
if ($ipv6) {
$docker_compose['services'][$this->application->uuid]['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);
} }
} }
@@ -1539,18 +1548,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 {
@@ -1559,7 +1568,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],
); );
} }
} }
@@ -1680,7 +1689,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
// 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]
); );
} }
} }

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

@@ -51,6 +51,9 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
{ {
$this->backup = $backup; $this->backup = $backup;
$this->team = Team::find($backup->team_id); $this->team = Team::find($backup->team_id);
if (is_null($this->team)) {
return;
}
if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') { if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') {
$this->database = data_get($this->backup, 'database'); $this->database = data_get($this->backup, 'database');
$this->server = $this->database->service->server; $this->server = $this->database->service->server;
@@ -316,7 +319,7 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
private function backup_standalone_mongodb(string $databaseWithCollections): void private function backup_standalone_mongodb(string $databaseWithCollections): void
{ {
try { try {
$url = $this->database->getDbUrl(useInternal: true); $url = $this->database->get_db_url(useInternal: true);
if ($databaseWithCollections === 'all') { if ($databaseWithCollections === 'all') {
$commands[] = "mkdir -p " . $this->backup_dir; $commands[] = "mkdir -p " . $this->backup_dir;
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --gzip --archive > $this->backup_location"; $commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --gzip --archive > $this->backup_location";

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

@@ -4,6 +4,7 @@ namespace App\Jobs;
use App\Actions\Server\CleanupDocker; use App\Actions\Server\CleanupDocker;
use App\Models\Server; use App\Models\Server;
use App\Notifications\Server\DockerCleanup;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
@@ -47,8 +48,9 @@ class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted
CleanupDocker::run($this->server); CleanupDocker::run($this->server);
$usageAfter = $this->server->getDiskUsage(); $usageAfter = $this->server->getDiskUsage();
if ($usageAfter < $this->usageBefore) { if ($usageAfter < $this->usageBefore) {
ray('Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name); $this->server->team?->notify(new DockerCleanup($this->server, 'Saved ' . ($this->usageBefore - $usageAfter) . '% disk space.'));
send_internal_notification('DockerCleanupJob done: Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name); // ray('Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name);
// send_internal_notification('DockerCleanupJob done: Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name);
Log::info('DockerCleanupJob done: Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name); Log::info('DockerCleanupJob done: Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name);
} else { } else {
Log::info('DockerCleanupJob failed to save disk space on ' . $this->server->name); Log::info('DockerCleanupJob failed to save disk space on ' . $this->server->name);

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

@@ -122,7 +122,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
} }
$this->selectedExistingPrivateKey = $this->createdServer->privateKey->id; $this->selectedExistingPrivateKey = $this->createdServer->privateKey->id;
$this->serverPublicKey = $this->createdServer->privateKey->publicKey(); $this->serverPublicKey = $this->createdServer->privateKey->publicKey();
$this->installServer(); $this->currentState = 'validate-server';
} }
public function getProxyType() public function getProxyType()
{ {

View File

@@ -9,6 +9,8 @@ class Advanced extends Component
{ {
public Application $application; public Application $application;
public bool $is_force_https_enabled; public bool $is_force_https_enabled;
public bool $is_gzip_enabled;
public bool $is_stripprefix_enabled;
protected $rules = [ protected $rules = [
'application.settings.is_git_submodules_enabled' => 'boolean|required', 'application.settings.is_git_submodules_enabled' => 'boolean|required',
'application.settings.is_git_lfs_enabled' => 'boolean|required', 'application.settings.is_git_lfs_enabled' => 'boolean|required',
@@ -19,13 +21,17 @@ class Advanced extends Component
'application.settings.is_gpu_enabled' => 'boolean|required', 'application.settings.is_gpu_enabled' => 'boolean|required',
'application.settings.is_build_server_enabled' => 'boolean|required', 'application.settings.is_build_server_enabled' => 'boolean|required',
'application.settings.is_consistent_container_name_enabled' => 'boolean|required', 'application.settings.is_consistent_container_name_enabled' => 'boolean|required',
'application.settings.is_gzip_enabled' => 'boolean|required',
'application.settings.is_stripprefix_enabled' => 'boolean|required',
'application.settings.gpu_driver' => 'string|required', 'application.settings.gpu_driver' => 'string|required',
'application.settings.gpu_count' => 'string|required', 'application.settings.gpu_count' => 'string|required',
'application.settings.gpu_device_ids' => 'string|required', 'application.settings.gpu_device_ids' => 'string|required',
'application.settings.gpu_options' => 'string|required', 'application.settings.gpu_options' => 'string|required',
]; ];
public function mount() { public function mount() {
$this->is_force_https_enabled = $this->application->settings->is_force_https_enabled; $this->is_force_https_enabled = $this->application->isForceHttpsEnabled();
$this->is_gzip_enabled = $this->application->isGzipEnabled();
$this->is_stripprefix_enabled = $this->application->isStripprefixEnabled();
} }
public function instantSave() public function instantSave()
{ {
@@ -40,6 +46,14 @@ class Advanced extends Component
$this->application->settings->is_force_https_enabled = $this->is_force_https_enabled; $this->application->settings->is_force_https_enabled = $this->is_force_https_enabled;
$this->dispatch('resetDefaultLabels', false); $this->dispatch('resetDefaultLabels', false);
} }
if ($this->application->settings->is_gzip_enabled !== $this->is_gzip_enabled) {
$this->application->settings->is_gzip_enabled = $this->is_gzip_enabled;
$this->dispatch('resetDefaultLabels', false);
}
if ($this->application->settings->is_stripprefix_enabled !== $this->is_stripprefix_enabled) {
$this->application->settings->is_stripprefix_enabled = $this->is_stripprefix_enabled;
$this->dispatch('resetDefaultLabels', false);
}
$this->application->settings->save(); $this->application->settings->save();
$this->dispatch('success', 'Settings saved.'); $this->dispatch('success', 'Settings saved.');
} }

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()
{ {
@@ -263,7 +263,11 @@ class General extends Component
} }
if ($this->application->build_pack === 'dockercompose') { if ($this->application->build_pack === 'dockercompose') {
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains); $this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
$this->parsedServices = $this->application->parseCompose(); if ($this->application->settings->is_raw_compose_deployment_enabled) {
$this->application->parseRawCompose();
} else {
$this->parsedServices = $this->application->parseCompose();
}
} }
$this->application->custom_labels = base64_encode($this->customLabels); $this->application->custom_labels = base64_encode($this->customLabels);
$this->application->save(); $this->application->save();

View File

@@ -46,9 +46,9 @@ class General extends Component
public function mount() public function mount()
{ {
$this->db_url = $this->database->getDbUrl(true); $this->db_url = $this->database->get_db_url(true);
if ($this->database->is_public) { if ($this->database->is_public) {
$this->db_url_public = $this->database->getDbUrl(); $this->db_url_public = $this->database->get_db_url();
} }
} }
public function instantSaveAdvanced() { public function instantSaveAdvanced() {
@@ -93,7 +93,7 @@ class General extends Component
return; return;
} }
StartDatabaseProxy::run($this->database); StartDatabaseProxy::run($this->database);
$this->db_url_public = $this->database->getDbUrl(); $this->db_url_public = $this->database->get_db_url();
$this->dispatch('success', 'Database is now publicly accessible.'); $this->dispatch('success', 'Database is now publicly accessible.');
} else { } else {
StopDatabaseProxy::run($this->database); StopDatabaseProxy::run($this->database);

View File

@@ -44,9 +44,9 @@ class General extends Component
public function mount() public function mount()
{ {
$this->db_url = $this->database->getDbUrl(true); $this->db_url = $this->database->get_db_url(true);
if ($this->database->is_public) { if ($this->database->is_public) {
$this->db_url_public = $this->database->getDbUrl(); $this->db_url_public = $this->database->get_db_url();
} }
} }
public function instantSaveAdvanced() public function instantSaveAdvanced()
@@ -95,7 +95,7 @@ class General extends Component
return; return;
} }
StartDatabaseProxy::run($this->database); StartDatabaseProxy::run($this->database);
$this->db_url_public = $this->database->getDbUrl(); $this->db_url_public = $this->database->get_db_url();
$this->dispatch('success', 'Database is now publicly accessible.'); $this->dispatch('success', 'Database is now publicly accessible.');
} else { } else {
StopDatabaseProxy::run($this->database); StopDatabaseProxy::run($this->database);

View File

@@ -46,9 +46,9 @@ class General extends Component
public function mount() public function mount()
{ {
$this->db_url = $this->database->getDbUrl(true); $this->db_url = $this->database->get_db_url(true);
if ($this->database->is_public) { if ($this->database->is_public) {
$this->db_url_public = $this->database->getDbUrl(); $this->db_url_public = $this->database->get_db_url();
} }
} }
public function instantSaveAdvanced() public function instantSaveAdvanced()
@@ -94,7 +94,7 @@ class General extends Component
return; return;
} }
StartDatabaseProxy::run($this->database); StartDatabaseProxy::run($this->database);
$this->db_url_public = $this->database->getDbUrl(); $this->db_url_public = $this->database->get_db_url();
$this->dispatch('success', 'Database is now publicly accessible.'); $this->dispatch('success', 'Database is now publicly accessible.');
} else { } else {
StopDatabaseProxy::run($this->database); StopDatabaseProxy::run($this->database);

View File

@@ -53,9 +53,9 @@ class General extends Component
]; ];
public function mount() public function mount()
{ {
$this->db_url = $this->database->getDbUrl(true); $this->db_url = $this->database->get_db_url(true);
if ($this->database->is_public) { if ($this->database->is_public) {
$this->db_url_public = $this->database->getDbUrl(); $this->db_url_public = $this->database->get_db_url();
} }
} }
public function instantSaveAdvanced() { public function instantSaveAdvanced() {
@@ -87,7 +87,7 @@ class General extends Component
return; return;
} }
StartDatabaseProxy::run($this->database); StartDatabaseProxy::run($this->database);
$this->db_url_public = $this->database->getDbUrl(); $this->db_url_public = $this->database->get_db_url();
$this->dispatch('success', 'Database is now publicly accessible.'); $this->dispatch('success', 'Database is now publicly accessible.');
} else { } else {
StopDatabaseProxy::run($this->database); StopDatabaseProxy::run($this->database);

View File

@@ -39,9 +39,9 @@ class General extends Component
]; ];
public function mount() public function mount()
{ {
$this->db_url = $this->database->getDbUrl(true); $this->db_url = $this->database->get_db_url(true);
if ($this->database->is_public) { if ($this->database->is_public) {
$this->db_url_public = $this->database->getDbUrl(); $this->db_url_public = $this->database->get_db_url();
} }
} }
public function instantSaveAdvanced() { public function instantSaveAdvanced() {
@@ -86,7 +86,7 @@ class General extends Component
return; return;
} }
StartDatabaseProxy::run($this->database); StartDatabaseProxy::run($this->database);
$this->db_url_public = $this->database->getDbUrl(); $this->db_url_public = $this->database->get_db_url();
$this->dispatch('success', 'Database is now publicly accessible.'); $this->dispatch('success', 'Database is now publicly accessible.');
} else { } else {
StopDatabaseProxy::run($this->database); StopDatabaseProxy::run($this->database);

View File

@@ -79,7 +79,10 @@ class GithubPrivateRepository extends Component
$this->loadRepositoryByPage(); $this->loadRepositoryByPage();
} }
} }
$this->selected_repository_id = $this->repositories[0]['id']; $this->repositories = $this->repositories->sortBy('name');
if ($this->repositories->count() > 0) {
$this->selected_repository_id = data_get($this->repositories->first(), 'id');
}
$this->current_step = 'repository'; $this->current_step = 'repository';
} }

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,8 @@ 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", "check_status"
"checkStatus",
]; ];
} }
public function render() public function render()
@@ -37,21 +36,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

@@ -6,7 +6,6 @@ use App\Actions\Shared\PullImage;
use App\Actions\Service\StartService; use App\Actions\Service\StartService;
use App\Actions\Service\StopService; use App\Actions\Service\StopService;
use App\Events\ServiceStatusChanged; use App\Events\ServiceStatusChanged;
use App\Jobs\ContainerStatusJob;
use App\Models\Service; use App\Models\Service;
use Livewire\Component; use Livewire\Component;
use Spatie\Activitylog\Models\Activity; use Spatie\Activitylog\Models\Activity;
@@ -17,7 +16,24 @@ 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 check_status() {
$this->dispatch('check_status');
$this->dispatch('success', 'Service status updated.');
}
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 +44,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,7 +58,6 @@ 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', 'Containers cleaned up.'); $this->dispatch('success', 'Containers cleaned up.');
} else { } else {

View File

@@ -18,6 +18,7 @@ class ServiceApplicationView extends Component
'application.required_fqdn' => 'required|boolean', 'application.required_fqdn' => 'required|boolean',
'application.is_log_drain_enabled' => 'nullable|boolean', 'application.is_log_drain_enabled' => 'nullable|boolean',
'application.is_gzip_enabled' => 'nullable|boolean', 'application.is_gzip_enabled' => 'nullable|boolean',
'application.is_stripprefix_enabled' => 'nullable|boolean',
]; ];
public function render() public function render()
{ {

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,47 +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 {
if ($this->server->isForceDisabled()) { 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.'); throw new \RuntimeException('Server is disabled.');
} }
// Wrap command to prevent escaped execution in the host.
$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,84 +10,99 @@ 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);
}); }
} }
} else if (data_get($this->parameters, 'database_uuid')) { public function mount()
$this->type = 'database'; {
$resource = StandalonePostgresql::where('uuid', $this->parameters['database_uuid'])->first(); try {
if (is_null($resource)) { $this->containers = collect();
$resource = StandaloneRedis::where('uuid', $this->parameters['database_uuid'])->first(); $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')) {
$this->type = 'database';
$resource = StandalonePostgresql::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) { if (is_null($resource)) {
$resource = StandaloneMongodb::where('uuid', $this->parameters['database_uuid'])->first(); $resource = StandaloneRedis::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) { if (is_null($resource)) {
$resource = StandaloneMysql::where('uuid', $this->parameters['database_uuid'])->first(); $resource = StandaloneMongodb::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) { if (is_null($resource)) {
$resource = StandaloneMariadb::where('uuid', $this->parameters['database_uuid'])->first(); $resource = StandaloneMysql::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) { if (is_null($resource)) {
abort(404); $resource = StandaloneMariadb::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
abort(404);
}
} }
} }
} }
} }
} $this->resource = $resource;
$this->resource = $resource; $this->status = $this->resource->status;
$this->status = $this->resource->status; if ($this->resource->destination->server->isFunctional()) {
$this->server = $this->resource->destination->server; $this->servers = $this->servers->push($this->resource->destination->server);
$this->container = $this->resource->uuid; }
// if (str(data_get($this, 'resource.status'))->startsWith('running')) { $this->container = $this->resource->uuid;
$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);
}
}
} catch (\Exception $e) {
return handleError($e, $this);
} }
} }

View File

@@ -4,6 +4,7 @@ namespace App\Livewire\Server;
use App\Actions\Proxy\CheckConfiguration; use App\Actions\Proxy\CheckConfiguration;
use App\Actions\Proxy\SaveConfiguration; use App\Actions\Proxy\SaveConfiguration;
use App\Actions\Proxy\StartProxy;
use App\Models\Server; use App\Models\Server;
use Livewire\Component; use Livewire\Component;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@@ -26,7 +27,7 @@ class Proxy extends Component
public function proxyStatusUpdated() public function proxyStatusUpdated()
{ {
$this->server->refresh(); $this->dispatch('refresh')->self();
} }
public function change_proxy() public function change_proxy()
@@ -41,6 +42,9 @@ class Proxy extends Component
$this->server->proxy->set('type', $proxy_type); $this->server->proxy->set('type', $proxy_type);
$this->server->save(); $this->server->save();
$this->selectedProxy = $this->server->proxy->type; $this->selectedProxy = $this->server->proxy->type;
if ($this->selectedProxy !== 'NONE') {
StartProxy::run($this->server, false);
}
$this->dispatch('proxyStatusUpdated'); $this->dispatch('proxyStatusUpdated');
} }

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,7 +2,7 @@
namespace App\Livewire\Tags; namespace App\Livewire\Tags;
use App\Http\Controllers\Api\APIDeploy as Deploy; use App\Http\Controllers\Api\Deploy;
use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationDeploymentQueue;
use App\Models\Tag; use App\Models\Tag;
use Livewire\Component; use Livewire\Component;

View File

@@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
use Spatie\Activitylog\Models\Activity; use Spatie\Activitylog\Models\Activity;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use RuntimeException; use RuntimeException;
use Symfony\Component\Yaml\Yaml;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
class Application extends BaseModel class Application extends BaseModel
@@ -65,6 +66,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')) {
@@ -72,6 +80,18 @@ class Application extends BaseModel
} }
return false; return false;
} }
public function isForceHttpsEnabled()
{
return data_get($this, 'settings.is_force_https_enabled', false);
}
public function isStripprefixEnabled()
{
return data_get($this, 'settings.is_stripprefix_enabled', true);
}
public function isGzipEnabled()
{
return data_get($this, 'settings.is_gzip_enabled', true);
}
public function link() public function link()
{ {
if (data_get($this, 'environment.project.uuid')) { if (data_get($this, 'environment.project.uuid')) {
@@ -395,7 +415,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');
@@ -466,6 +489,10 @@ class Application extends BaseModel
} }
return false; return false;
} }
public function workdir()
{
return application_configuration_dir() . "/{$this->uuid}";
}
public function isLogDrainEnabled() public function isLogDrainEnabled()
{ {
return data_get($this, 'settings.is_log_drain_enabled', false); return data_get($this, 'settings.is_log_drain_enabled', false);
@@ -700,6 +727,64 @@ class Application extends BaseModel
]; ];
} }
} }
function parseRawCompose()
{
try {
$yaml = Yaml::parse($this->docker_compose_raw);
} catch (\Exception $e) {
throw new \Exception($e->getMessage());
}
$services = data_get($yaml, 'services');
$commands = collect([]);
$services = collect($services)->map(function ($service) use ($commands) {
$serviceVolumes = collect(data_get($service, 'volumes', []));
if ($serviceVolumes->count() > 0) {
foreach ($serviceVolumes as $volume) {
$workdir = $this->workdir();
$type = null;
$source = null;
if (is_string($volume)) {
$source = Str::of($volume)->before(':');
if ($source->startsWith('./') || $source->startsWith('/') || $source->startsWith('~')) {
$type = Str::of('bind');
}
} else if (is_array($volume)) {
$type = data_get_str($volume, 'type');
$source = data_get_str($volume, 'source');
}
if ($type->value() === 'bind') {
if ($source->value() === "/var/run/docker.sock") {
continue;
}
if ($source->value() === '/tmp' || $source->value() === '/tmp/') {
continue;
}
if ($source->startsWith('.')) {
$source = $source->after('.');
$source = $workdir . $source;
}
$commands->push("mkdir -p $source > /dev/null 2>&1 || true");
}
}
}
$labels = collect(data_get($service, 'labels', []));
if (!$labels->contains('coolify.managed')) {
$labels->push('coolify.managed=true');
}
if (!$labels->contains('coolify.applicationId')) {
$labels->push('coolify.applicationId=' . $this->id);
}
if (!$labels->contains('coolify.type')) {
$labels->push('coolify.type=application');
}
data_set($service, 'labels', $labels->toArray());
return $service;
});
data_set($yaml, 'services', $services->toArray());
$this->docker_compose_raw = Yaml::dump($yaml, 10, 2);
instant_remote_process($commands, $this->destination->server, false);
}
function parseCompose(int $pull_request_id = 0) function parseCompose(int $pull_request_id = 0)
{ {
if ($this->docker_compose_raw) { if ($this->docker_compose_raw) {

View File

@@ -3,7 +3,6 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Support\Str;
class LocalFileVolume extends BaseModel class LocalFileVolume extends BaseModel
{ {

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

@@ -263,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()
{ {
@@ -361,8 +365,9 @@ class Server extends BaseModel
{ {
$standalone_docker = $this->hasMany(StandaloneDocker::class)->get(); $standalone_docker = $this->hasMany(StandaloneDocker::class)->get();
$swarm_docker = $this->hasMany(SwarmDocker::class)->get(); $swarm_docker = $this->hasMany(SwarmDocker::class)->get();
$asd = $this->belongsToMany(StandaloneDocker::class, 'additional_destinations')->withPivot('server_id')->get(); // $additional_dockers = $this->belongsToMany(StandaloneDocker::class, 'additional_destinations')->withPivot('server_id')->get();
return $standalone_docker->concat($swarm_docker)->concat($asd); // return $standalone_docker->concat($swarm_docker)->concat($additional_dockers);
return $standalone_docker->concat($swarm_docker);
} }
public function standaloneDockers() public function standaloneDockers()

View File

@@ -102,6 +102,56 @@ 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('directus'):
$data = collect([]);
$admin_email = $this->environment_variables()->where('key', 'ADMIN_EMAIL')->first();
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ADMIN')->first();
if ($admin_email) {
$data = $data->merge([
'Admin Email' => [
'key' => data_get($admin_email, 'key'),
'value' => data_get($admin_email, 'value'),
'rules' => 'required|email',
],
]);
}
if ($admin_password) {
$data = $data->merge([
'Admin Password' => [
'key' => data_get($admin_password, 'key'),
'value' => data_get($admin_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
}
$fields->put('Directus', $data);
break;
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

@@ -23,6 +23,10 @@ class ServiceApplication extends BaseModel
{ {
return data_get($this, 'is_log_drain_enabled', false); return data_get($this, 'is_log_drain_enabled', false);
} }
public function isStripprefixEnabled()
{
return data_get($this, 'is_stripprefix_enabled', true);
}
public function isGzipEnabled() public function isGzipEnabled()
{ {
return data_get($this, 'is_gzip_enabled', true); return data_get($this, 'is_gzip_enabled', true);

View File

@@ -21,9 +21,13 @@ class ServiceDatabase extends BaseModel
{ {
return data_get($this, 'is_log_drain_enabled', false); return data_get($this, 'is_log_drain_enabled', false);
} }
public function isStripprefixEnabled()
{
return data_get($this, 'is_stripprefix_enabled', true);
}
public function isGzipEnabled() public function isGzipEnabled()
{ {
return true; return data_get($this, 'is_gzip_enabled', true);
} }
public function type() public function type()
{ {

View File

@@ -126,7 +126,7 @@ class StandaloneMariadb extends BaseModel
); );
} }
public function getDbUrl(bool $useInternal = false): string public function get_db_url(bool $useInternal = false): string
{ {
if ($this->is_public && !$useInternal) { if ($this->is_public && !$useInternal) {
return "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mariadb_database}"; return "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mariadb_database}";

View File

@@ -142,7 +142,7 @@ class StandaloneMongodb extends BaseModel
{ {
return 'standalone-mongodb'; return 'standalone-mongodb';
} }
public function getDbUrl(bool $useInternal = false) public function get_db_url(bool $useInternal = false)
{ {
if ($this->is_public && !$useInternal) { if ($this->is_public && !$useInternal) {
return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->destination->server->getIp}:{$this->public_port}/?directConnection=true"; return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->destination->server->getIp}:{$this->public_port}/?directConnection=true";

View File

@@ -127,7 +127,7 @@ class StandaloneMysql extends BaseModel
); );
} }
public function getDbUrl(bool $useInternal = false): string public function get_db_url(bool $useInternal = false): string
{ {
if ($this->is_public && !$useInternal) { if ($this->is_public && !$useInternal) {
return "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mysql_database}"; return "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mysql_database}";

View File

@@ -126,7 +126,7 @@ class StandalonePostgresql extends BaseModel
{ {
return 'standalone-postgresql'; return 'standalone-postgresql';
} }
public function getDbUrl(bool $useInternal = false): string public function get_db_url(bool $useInternal = false): string
{ {
if ($this->is_public && !$useInternal) { if ($this->is_public && !$useInternal) {
return "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->postgres_db}"; return "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->postgres_db}";

View File

@@ -122,7 +122,7 @@ class StandaloneRedis extends BaseModel
{ {
return 'standalone-redis'; return 'standalone-redis';
} }
public function getDbUrl(bool $useInternal = false): string public function get_db_url(bool $useInternal = false): string
{ {
if ($this->is_public && !$useInternal) { if ($this->is_public && !$useInternal) {
return "redis://:{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0"; return "redis://:{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0";

View File

@@ -62,6 +62,9 @@ class Team extends Model implements SendsDiscord, SendsEmail
} }
static public function serverLimit() static public function serverLimit()
{ {
if (currentTeam()->id === 0 && isDev()) {
return 9999999;
}
return Team::find(currentTeam()->id)->limits['serverLimit']; return Team::find(currentTeam()->id)->limits['serverLimit'];
} }
public function limits(): Attribute public function limits(): Attribute
@@ -141,7 +144,6 @@ class Team extends Model implements SendsDiscord, SendsEmail
$sources = collect([]); $sources = collect([]);
$github_apps = $this->hasMany(GithubApp::class)->whereisPublic(false)->get(); $github_apps = $this->hasMany(GithubApp::class)->whereisPublic(false)->get();
$gitlab_apps = $this->hasMany(GitlabApp::class)->whereisPublic(false)->get(); $gitlab_apps = $this->hasMany(GitlabApp::class)->whereisPublic(false)->get();
// $bitbucket_apps = $this->hasMany(BitbucketApp::class)->get();
$sources = $sources->merge($github_apps)->merge($gitlab_apps); $sources = $sources->merge($github_apps)->merge($gitlab_apps);
return $sources; return $sources;
} }

View File

@@ -26,6 +26,7 @@ class TeamInvitation extends Model
return true; return true;
} else { } else {
$this->delete(); $this->delete();
return false;
} }
} }
} }

View File

@@ -50,6 +50,21 @@ class User extends Authenticatable implements SendsEmail
$user->teams()->attach($new_team, ['role' => 'owner']); $user->teams()->attach($new_team, ['role' => 'owner']);
}); });
} }
public function recreate_personal_team()
{
$team = [
'name' => $this->name . "'s Team",
'personal_team' => true,
'show_boarding' => true
];
if ($this->id === 0) {
$team['id'] = 0;
$team['name'] = 'Root Team';
}
$new_team = Team::create($team);
$this->teams()->attach($new_team, ['role' => 'owner']);
return $new_team;
}
public function createToken(string $name, array $abilities = ['*'], DateTimeInterface $expiresAt = null) public function createToken(string $name, array $abilities = ['*'], DateTimeInterface $expiresAt = null)
{ {
$plainTextToken = sprintf( $plainTextToken = sprintf(
@@ -143,7 +158,7 @@ class User extends Authenticatable implements SendsEmail
public function currentTeam() public function currentTeam()
{ {
return Cache::remember('team:' . auth()->user()->id, 3600, function () { return Cache::remember('team:' . auth()->user()->id, 3600, function () {
if (is_null(data_get(session('currentTeam'), 'id'))) { if (is_null(data_get(session('currentTeam'), 'id')) && auth()->user()->teams->count() > 0){
return auth()->user()->teams[0]; return auth()->user()->teams[0];
} }
return Team::find(session('currentTeam')->id); return Team::find(session('currentTeam')->id);

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\TelegramChannel;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;
class DockerCleanup extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 1;
public function __construct(public Server $server, public string $message)
{
}
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}) high disk usage detected!");
// $mail->view('emails.high-disk-usage', [
// 'name' => $this->server->name,
// 'disk_usage' => $this->disk_usage,
// 'threshold' => $this->cleanup_after_percentage,
// ]);
// return $mail;
// }
public function toDiscord(): string
{
$message = "Coolify: Server '{$this->server->name}' cleanup job done!\n\n{$this->message}";
return $message;
}
public function toTelegram(): array
{
return [
"message" => "Coolify: Server '{$this->server->name}' cleanup job done!\n\n{$this->message}"
];
}
}

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,14 +8,12 @@ use App\Actions\Fortify\UpdateUserPassword;
use App\Actions\Fortify\UpdateUserProfileInformation; use App\Actions\Fortify\UpdateUserProfileInformation;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use App\Models\User; use App\Models\User;
use App\Models\Waitlist;
use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Laravel\Fortify\Contracts\RegisterResponse; use Laravel\Fortify\Contracts\RegisterResponse;
use Laravel\Fortify\Features;
use Laravel\Fortify\Fortify; use Laravel\Fortify\Fortify;
class FortifyServiceProvider extends ServiceProvider class FortifyServiceProvider extends ServiceProvider
@@ -76,7 +74,11 @@ class FortifyServiceProvider extends ServiceProvider
) { ) {
$user->updated_at = now(); $user->updated_at = now();
$user->save(); $user->save();
session(['currentTeam' => $user->currentTeam = $user->teams->firstWhere('personal_team', true)]); $user->currentTeam = $user->teams->firstWhere('personal_team', true);
if (!$user->currentTeam) {
$user->currentTeam = $user->recreate_personal_team();
}
session(['currentTeam' => $user->currentTeam]);
return $user; return $user;
} }
}); });

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;
} }
@@ -212,7 +215,7 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource,
} }
return $payload; return $payload;
} }
function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?string $service_name = null) function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null)
{ {
$labels = collect([]); $labels = collect([]);
$labels->push('traefik.enable=true'); $labels->push('traefik.enable=true');
@@ -278,8 +281,10 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels->push("traefik.http.services.{$https_label}.loadbalancer.server.port=$port"); $labels->push("traefik.http.services.{$https_label}.loadbalancer.server.port=$port");
} }
if ($path !== '/') { if ($path !== '/') {
$labels->push("traefik.http.middlewares.{$https_label}-stripprefix.stripprefix.prefixes={$path}"); if ($is_stripprefix_enabled) {
$middlewares = collect(["{$https_label}-stripprefix"]); $labels->push("traefik.http.middlewares.{$https_label}-stripprefix.stripprefix.prefixes={$path}");
$middlewares = collect(["{$https_label}-stripprefix"]);
}
if ($is_gzip_enabled) { if ($is_gzip_enabled) {
$middlewares->push('gzip'); $middlewares->push('gzip');
} }
@@ -331,8 +336,10 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels->push("traefik.http.routers.{$http_label}.service={$http_label}"); $labels->push("traefik.http.routers.{$http_label}.service={$http_label}");
} }
if ($path !== '/') { if ($path !== '/') {
$labels->push("traefik.http.middlewares.{$http_label}-stripprefix.stripprefix.prefixes={$path}"); if ($is_stripprefix_enabled) {
$middlewares = collect(["{$http_label}-stripprefix"]); $labels->push("traefik.http.middlewares.{$http_label}-stripprefix.stripprefix.prefixes={$path}");
$middlewares = collect(["{$http_label}-stripprefix"]);
}
if ($is_gzip_enabled) { if ($is_gzip_enabled) {
$middlewares->push('gzip'); $middlewares->push('gzip');
} }
@@ -389,7 +396,14 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
$domains = Str::of(data_get($application, 'fqdn'))->explode(','); $domains = Str::of(data_get($application, 'fqdn'))->explode(',');
} }
// Add Traefik labels no matter which proxy is selected // Add Traefik labels no matter which proxy is selected
$labels = $labels->merge(fqdnLabelsForTraefik($appUuid, $domains, $application->settings->is_force_https_enabled, $onlyPort)); $labels = $labels->merge(fqdnLabelsForTraefik(
uuid: $appUuid,
domains: $domains,
onlyPort: $onlyPort,
is_force_https_enabled: $application->isForceHttpsEnabled(),
is_gzip_enabled: $application->isGzipEnabled(),
is_stripprefix_enabled: $application->isStripprefixEnabled()
));
} }
return $labels->all(); return $labels->all();
} }

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,
@@ -1036,7 +1047,15 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$serviceLabels = $serviceLabels->merge($defaultLabels); $serviceLabels = $serviceLabels->merge($defaultLabels);
if (!$isDatabase && $fqdns->count() > 0) { if (!$isDatabase && $fqdns->count() > 0) {
if ($fqdns) { if ($fqdns) {
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($resource->uuid, $fqdns, true, serviceLabels: $serviceLabels, is_gzip_enabled: $savedService->isGzipEnabled(), service_name: $serviceName)); $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik(
uuid: $resource->uuid,
domains: $fqdns,
is_force_https_enabled: true,
serviceLabels: $serviceLabels,
is_gzip_enabled: $savedService->isGzipEnabled(),
is_stripprefix_enabled: $savedService->isStripprefixEnabled(),
service_name: $serviceName
));
} }
} }
if ($resource->server->isLogDrainEnabled() && $savedService->isLogDrainEnabled()) { if ($resource->server->isLogDrainEnabled() && $savedService->isLogDrainEnabled()) {
@@ -1238,7 +1257,6 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
// Collect/create/update networks // Collect/create/update networks
if ($serviceNetworks->count() > 0) { if ($serviceNetworks->count() > 0) {
foreach ($serviceNetworks as $networkName => $networkDetails) { foreach ($serviceNetworks as $networkName => $networkDetails) {
ray($networkDetails);
$networkExists = $topLevelNetworks->contains(function ($value, $key) use ($networkName) { $networkExists = $topLevelNetworks->contains(function ($value, $key) use ($networkName) {
return $value == $networkName || $key == $networkName; return $value == $networkName || $key == $networkName;
}); });
@@ -1570,7 +1588,7 @@ function parseEnvVariable(Str|string $value)
'port' => $port, 'port' => $port,
]; ];
} }
function generateEnvValue(string $command) function generateEnvValue(string $command, ?Service $service = null)
{ {
switch ($command) { switch ($command) {
case 'PASSWORD': case 'PASSWORD':
@@ -1579,6 +1597,7 @@ function generateEnvValue(string $command)
case 'PASSWORD_64': case 'PASSWORD_64':
$generatedValue = Str::password(length: 64, symbols: false); $generatedValue = Str::password(length: 64, symbols: false);
break; break;
// This is not base64, it's just a random string
case 'BASE64_64': case 'BASE64_64':
$generatedValue = Str::random(64); $generatedValue = Str::random(64);
break; break;
@@ -1586,11 +1605,63 @@ function generateEnvValue(string $command)
$generatedValue = Str::random(128); $generatedValue = Str::random(128);
break; break;
case 'BASE64': case 'BASE64':
case 'BASE64_32':
$generatedValue = Str::random(32); $generatedValue = Str::random(32);
break; break;
// This is base64,
case 'REALBASE64_64':
$generatedValue = base64_encode(Str::random(64));
break;
case 'REALBASE64_128':
$generatedValue = base64_encode(Str::random(128));
break;
case 'REALBASE64':
case 'REALBASE64_32':
$generatedValue = base64_encode(Str::random(32));
break;
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;

566
composer.lock generated

File diff suppressed because it is too large Load Diff

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.226', 'release' => '4.0.0-beta.235',
// 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.226'; return '4.0.0-beta.235';

View File

@@ -0,0 +1,44 @@
<?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('application_settings', function (Blueprint $table) {
$table->boolean('is_gzip_enabled')->default(true);
$table->boolean('is_stripprefix_enabled')->default(true);
});
Schema::table('service_applications', function (Blueprint $table) {
$table->boolean('is_stripprefix_enabled')->default(true);
});
Schema::table('service_databases', function (Blueprint $table) {
$table->boolean('is_gzip_enabled')->default(true);
$table->boolean('is_stripprefix_enabled')->default(true);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('application_settings', function (Blueprint $table) {
$table->dropColumn('is_gzip_enabled');
$table->dropColumn('is_stripprefix_enabled');
});
Schema::table('service_applications', function (Blueprint $table) {
$table->dropColumn('is_stripprefix_enabled');
});
Schema::table('service_databases', function (Blueprint $table) {
$table->dropColumn('is_gzip_enabled');
$table->dropColumn('is_stripprefix_enabled');
});
}
};

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:

212
package-lock.json generated
View File

@@ -13,15 +13,15 @@
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "4.5.1", "@vitejs/plugin-vue": "4.5.1",
"autoprefixer": "10.4.17", "autoprefixer": "10.4.18",
"axios": "1.6.7", "axios": "1.6.7",
"laravel-echo": "1.15.3", "laravel-echo": "1.16.0",
"laravel-vite-plugin": "0.8.1", "laravel-vite-plugin": "0.8.1",
"postcss": "8.4.35", "postcss": "8.4.35",
"pusher-js": "8.4.0-rc2", "pusher-js": "8.4.0-rc2",
"tailwindcss": "3.4.1", "tailwindcss": "3.4.1",
"vite": "4.5.2", "vite": "4.5.2",
"vue": "3.4.19" "vue": "3.4.21"
} }
}, },
"node_modules/@alloc/quick-lru": { "node_modules/@alloc/quick-lru": {
@@ -36,9 +36,9 @@
} }
}, },
"node_modules/@babel/parser": { "node_modules/@babel/parser": {
"version": "7.23.9", "version": "7.24.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz",
"integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", "integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==",
"dev": true, "dev": true,
"bin": { "bin": {
"parser": "bin/babel-parser.js" "parser": "bin/babel-parser.js"
@@ -524,77 +524,77 @@
} }
}, },
"node_modules/@vue/compiler-core": { "node_modules/@vue/compiler-core": {
"version": "3.4.19", "version": "3.4.21",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.19.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.21.tgz",
"integrity": "sha512-gj81785z0JNzRcU0Mq98E56e4ltO1yf8k5PQ+tV/7YHnbZkrM0fyFyuttnN8ngJZjbpofWE/m4qjKBiLl8Ju4w==", "integrity": "sha512-MjXawxZf2SbZszLPYxaFCjxfibYrzr3eYbKxwpLR9EQN+oaziSu3qKVbwBERj1IFIB8OLUewxB5m/BFzi613og==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/parser": "^7.23.9", "@babel/parser": "^7.23.9",
"@vue/shared": "3.4.19", "@vue/shared": "3.4.21",
"entities": "^4.5.0", "entities": "^4.5.0",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"source-map-js": "^1.0.2" "source-map-js": "^1.0.2"
} }
}, },
"node_modules/@vue/compiler-core/node_modules/@vue/shared": { "node_modules/@vue/compiler-core/node_modules/@vue/shared": {
"version": "3.4.19", "version": "3.4.21",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.19.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.21.tgz",
"integrity": "sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw==", "integrity": "sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g==",
"dev": true "dev": true
}, },
"node_modules/@vue/compiler-dom": { "node_modules/@vue/compiler-dom": {
"version": "3.4.19", "version": "3.4.21",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.19.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.21.tgz",
"integrity": "sha512-vm6+cogWrshjqEHTzIDCp72DKtea8Ry/QVpQRYoyTIg9k7QZDX6D8+HGURjtmatfgM8xgCFtJJaOlCaRYRK3QA==", "integrity": "sha512-IZC6FKowtT1sl0CR5DpXSiEB5ayw75oT2bma1BEhV7RRR1+cfwLrxc2Z8Zq/RGFzJ8w5r9QtCOvTjQgdn0IKmA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@vue/compiler-core": "3.4.19", "@vue/compiler-core": "3.4.21",
"@vue/shared": "3.4.19" "@vue/shared": "3.4.21"
} }
}, },
"node_modules/@vue/compiler-dom/node_modules/@vue/shared": { "node_modules/@vue/compiler-dom/node_modules/@vue/shared": {
"version": "3.4.19", "version": "3.4.21",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.19.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.21.tgz",
"integrity": "sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw==", "integrity": "sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g==",
"dev": true "dev": true
}, },
"node_modules/@vue/compiler-sfc": { "node_modules/@vue/compiler-sfc": {
"version": "3.4.19", "version": "3.4.21",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.19.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.21.tgz",
"integrity": "sha512-LQ3U4SN0DlvV0xhr1lUsgLCYlwQfUfetyPxkKYu7dkfvx7g3ojrGAkw0AERLOKYXuAGnqFsEuytkdcComei3Yg==", "integrity": "sha512-me7epoTxYlY+2CUM7hy9PCDdpMPfIwrOvAXud2Upk10g4YLv9UBW7kL798TvMeDhPthkZ0CONNrK2GoeI1ODiQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/parser": "^7.23.9", "@babel/parser": "^7.23.9",
"@vue/compiler-core": "3.4.19", "@vue/compiler-core": "3.4.21",
"@vue/compiler-dom": "3.4.19", "@vue/compiler-dom": "3.4.21",
"@vue/compiler-ssr": "3.4.19", "@vue/compiler-ssr": "3.4.21",
"@vue/shared": "3.4.19", "@vue/shared": "3.4.21",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"magic-string": "^0.30.6", "magic-string": "^0.30.7",
"postcss": "^8.4.33", "postcss": "^8.4.35",
"source-map-js": "^1.0.2" "source-map-js": "^1.0.2"
} }
}, },
"node_modules/@vue/compiler-sfc/node_modules/@vue/shared": { "node_modules/@vue/compiler-sfc/node_modules/@vue/shared": {
"version": "3.4.19", "version": "3.4.21",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.19.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.21.tgz",
"integrity": "sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw==", "integrity": "sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g==",
"dev": true "dev": true
}, },
"node_modules/@vue/compiler-ssr": { "node_modules/@vue/compiler-ssr": {
"version": "3.4.19", "version": "3.4.21",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.19.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.21.tgz",
"integrity": "sha512-P0PLKC4+u4OMJ8sinba/5Z/iDT84uMRRlrWzadgLA69opCpI1gG4N55qDSC+dedwq2fJtzmGald05LWR5TFfLw==", "integrity": "sha512-M5+9nI2lPpAsgXOGQobnIueVqc9sisBFexh5yMIMRAPYLa7+5wEJs8iqOZc1WAa9WQbx9GR2twgznU8LTIiZ4Q==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@vue/compiler-dom": "3.4.19", "@vue/compiler-dom": "3.4.21",
"@vue/shared": "3.4.19" "@vue/shared": "3.4.21"
} }
}, },
"node_modules/@vue/compiler-ssr/node_modules/@vue/shared": { "node_modules/@vue/compiler-ssr/node_modules/@vue/shared": {
"version": "3.4.19", "version": "3.4.21",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.19.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.21.tgz",
"integrity": "sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw==", "integrity": "sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g==",
"dev": true "dev": true
}, },
"node_modules/@vue/reactivity": { "node_modules/@vue/reactivity": {
@@ -606,64 +606,64 @@
} }
}, },
"node_modules/@vue/runtime-core": { "node_modules/@vue/runtime-core": {
"version": "3.4.19", "version": "3.4.21",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.19.tgz", "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.21.tgz",
"integrity": "sha512-/Z3tFwOrerJB/oyutmJGoYbuoadphDcJAd5jOuJE86THNZji9pYjZroQ2NFsZkTxOq0GJbb+s2kxTYToDiyZzw==", "integrity": "sha512-pQthsuYzE1XcGZznTKn73G0s14eCJcjaLvp3/DKeYWoFacD9glJoqlNBxt3W2c5S40t6CCcpPf+jG01N3ULyrA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@vue/reactivity": "3.4.19", "@vue/reactivity": "3.4.21",
"@vue/shared": "3.4.19" "@vue/shared": "3.4.21"
} }
}, },
"node_modules/@vue/runtime-core/node_modules/@vue/reactivity": { "node_modules/@vue/runtime-core/node_modules/@vue/reactivity": {
"version": "3.4.19", "version": "3.4.21",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.19.tgz", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.21.tgz",
"integrity": "sha512-+VcwrQvLZgEclGZRHx4O2XhyEEcKaBi50WbxdVItEezUf4fqRh838Ix6amWTdX0CNb/b6t3Gkz3eOebfcSt+UA==", "integrity": "sha512-UhenImdc0L0/4ahGCyEzc/pZNwVgcglGy9HVzJ1Bq2Mm9qXOpP8RyNTjookw/gOCUlXSEtuZ2fUg5nrHcoqJcw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@vue/shared": "3.4.19" "@vue/shared": "3.4.21"
} }
}, },
"node_modules/@vue/runtime-core/node_modules/@vue/shared": { "node_modules/@vue/runtime-core/node_modules/@vue/shared": {
"version": "3.4.19", "version": "3.4.21",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.19.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.21.tgz",
"integrity": "sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw==", "integrity": "sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g==",
"dev": true "dev": true
}, },
"node_modules/@vue/runtime-dom": { "node_modules/@vue/runtime-dom": {
"version": "3.4.19", "version": "3.4.21",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.19.tgz", "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.21.tgz",
"integrity": "sha512-IyZzIDqfNCF0OyZOauL+F4yzjMPN2rPd8nhqPP2N1lBn3kYqJpPHHru+83Rkvo2lHz5mW+rEeIMEF9qY3PB94g==", "integrity": "sha512-gvf+C9cFpevsQxbkRBS1NpU8CqxKw0ebqMvLwcGQrNpx6gqRDodqKqA+A2VZZpQ9RpK2f9yfg8VbW/EpdFUOJw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@vue/runtime-core": "3.4.19", "@vue/runtime-core": "3.4.21",
"@vue/shared": "3.4.19", "@vue/shared": "3.4.21",
"csstype": "^3.1.3" "csstype": "^3.1.3"
} }
}, },
"node_modules/@vue/runtime-dom/node_modules/@vue/shared": { "node_modules/@vue/runtime-dom/node_modules/@vue/shared": {
"version": "3.4.19", "version": "3.4.21",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.19.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.21.tgz",
"integrity": "sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw==", "integrity": "sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g==",
"dev": true "dev": true
}, },
"node_modules/@vue/server-renderer": { "node_modules/@vue/server-renderer": {
"version": "3.4.19", "version": "3.4.21",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.19.tgz", "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.21.tgz",
"integrity": "sha512-eAj2p0c429RZyyhtMRnttjcSToch+kTWxFPHlzGMkR28ZbF1PDlTcmGmlDxccBuqNd9iOQ7xPRPAGgPVj+YpQw==", "integrity": "sha512-aV1gXyKSN6Rz+6kZ6kr5+Ll14YzmIbeuWe7ryJl5muJ4uwSwY/aStXTixx76TwkZFJLm1aAlA/HSWEJ4EyiMkg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@vue/compiler-ssr": "3.4.19", "@vue/compiler-ssr": "3.4.21",
"@vue/shared": "3.4.19" "@vue/shared": "3.4.21"
}, },
"peerDependencies": { "peerDependencies": {
"vue": "3.4.19" "vue": "3.4.21"
} }
}, },
"node_modules/@vue/server-renderer/node_modules/@vue/shared": { "node_modules/@vue/server-renderer/node_modules/@vue/shared": {
"version": "3.4.19", "version": "3.4.21",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.19.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.21.tgz",
"integrity": "sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw==", "integrity": "sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g==",
"dev": true "dev": true
}, },
"node_modules/@vue/shared": { "node_modules/@vue/shared": {
@@ -708,9 +708,9 @@
"dev": true "dev": true
}, },
"node_modules/autoprefixer": { "node_modules/autoprefixer": {
"version": "10.4.17", "version": "10.4.18",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.18.tgz",
"integrity": "sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==", "integrity": "sha512-1DKbDfsr6KUElM6wg+0zRNkB/Q7WcKYAaK+pzXn+Xqmszm/5Xa9coeNdtP88Vi+dPzZnMjhge8GIV49ZQkDa+g==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -727,8 +727,8 @@
} }
], ],
"dependencies": { "dependencies": {
"browserslist": "^4.22.2", "browserslist": "^4.23.0",
"caniuse-lite": "^1.0.30001578", "caniuse-lite": "^1.0.30001591",
"fraction.js": "^4.3.7", "fraction.js": "^4.3.7",
"normalize-range": "^0.1.2", "normalize-range": "^0.1.2",
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
@@ -789,9 +789,9 @@
} }
}, },
"node_modules/browserslist": { "node_modules/browserslist": {
"version": "4.22.2", "version": "4.23.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
"integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -808,8 +808,8 @@
} }
], ],
"dependencies": { "dependencies": {
"caniuse-lite": "^1.0.30001565", "caniuse-lite": "^1.0.30001587",
"electron-to-chromium": "^1.4.601", "electron-to-chromium": "^1.4.668",
"node-releases": "^2.0.14", "node-releases": "^2.0.14",
"update-browserslist-db": "^1.0.13" "update-browserslist-db": "^1.0.13"
}, },
@@ -829,9 +829,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001580", "version": "1.0.30001594",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001580.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001594.tgz",
"integrity": "sha512-mtj5ur2FFPZcCEpXFy8ADXbDACuNFXg6mxVDqp7tqooX6l3zwm+d8EPoeOSIFRDvHs8qu7/SLFOGniULkcH2iA==", "integrity": "sha512-VblSX6nYqyJVs8DKFMldE2IVCJjZ225LW00ydtUWwh5hk9IfkTOffO6r8gJNsH0qqqeAF8KrbMYA2VEwTlGW5g==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -1014,9 +1014,9 @@
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.4.647", "version": "1.4.692",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.647.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.692.tgz",
"integrity": "sha512-Z/fTNGwc45WrYQhPaEcz5tAJuZZ8G7S/DBnhS6Kgp4BxnS40Z/HqlJ0hHg3Z79IGVzuVartIlTcjw/cQbPLgOw==", "integrity": "sha512-d5rZRka9n2Y3MkWRN74IoAsxR0HK3yaAt7T50e3iT9VZmCCQDT3geXUO5ZRMhDToa1pkCeQXuNo+0g+NfDOVPA==",
"dev": true "dev": true
}, },
"node_modules/entities": { "node_modules/entities": {
@@ -1069,9 +1069,9 @@
} }
}, },
"node_modules/escalade": { "node_modules/escalade": {
"version": "3.1.1", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
"integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=6" "node": ">=6"
@@ -1347,9 +1347,9 @@
} }
}, },
"node_modules/laravel-echo": { "node_modules/laravel-echo": {
"version": "1.15.3", "version": "1.16.0",
"resolved": "https://registry.npmjs.org/laravel-echo/-/laravel-echo-1.15.3.tgz", "resolved": "https://registry.npmjs.org/laravel-echo/-/laravel-echo-1.16.0.tgz",
"integrity": "sha512-SRXzccaat6w4qKgZ4/rjFKr3nJfVxB+ly4V0MEJNIF1/TpERNXepo3uk7NnOjBGsiV/np1fl2XitAzW4Sa1s/w==", "integrity": "sha512-BJGUa4tcKvYmTkzTmcBGMHiO2tq+k7Do5wPmLbRswWfzKwyfZEUR+J5iwBTPEfLLwNPZlA9Kjo6R/NV6pmyIpg==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=10" "node": ">=10"
@@ -1410,9 +1410,9 @@
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
}, },
"node_modules/magic-string": { "node_modules/magic-string": {
"version": "0.30.7", "version": "0.30.8",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz",
"integrity": "sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==", "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15" "@jridgewell/sourcemap-codec": "^1.4.15"
@@ -2106,16 +2106,16 @@
} }
}, },
"node_modules/vue": { "node_modules/vue": {
"version": "3.4.19", "version": "3.4.21",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.4.19.tgz", "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.21.tgz",
"integrity": "sha512-W/7Fc9KUkajFU8dBeDluM4sRGc/aa4YJnOYck8dkjgZoXtVsn3OeTGni66FV1l3+nvPA7VBFYtPioaGKUmEADw==", "integrity": "sha512-5hjyV/jLEIKD/jYl4cavMcnzKwjMKohureP8ejn3hhEjwhWIhWeuzL2kJAjzl/WyVsgPY56Sy4Z40C3lVshxXA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@vue/compiler-dom": "3.4.19", "@vue/compiler-dom": "3.4.21",
"@vue/compiler-sfc": "3.4.19", "@vue/compiler-sfc": "3.4.21",
"@vue/runtime-dom": "3.4.19", "@vue/runtime-dom": "3.4.21",
"@vue/server-renderer": "3.4.19", "@vue/server-renderer": "3.4.21",
"@vue/shared": "3.4.19" "@vue/shared": "3.4.21"
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "*" "typescript": "*"
@@ -2127,9 +2127,9 @@
} }
}, },
"node_modules/vue/node_modules/@vue/shared": { "node_modules/vue/node_modules/@vue/shared": {
"version": "3.4.19", "version": "3.4.21",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.19.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.21.tgz",
"integrity": "sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw==", "integrity": "sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g==",
"dev": true "dev": true
}, },
"node_modules/wrappy": { "node_modules/wrappy": {

View File

@@ -7,15 +7,15 @@
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "4.5.1", "@vitejs/plugin-vue": "4.5.1",
"autoprefixer": "10.4.17", "autoprefixer": "10.4.18",
"axios": "1.6.7", "axios": "1.6.7",
"laravel-echo": "1.15.3", "laravel-echo": "1.16.0",
"laravel-vite-plugin": "0.8.1", "laravel-vite-plugin": "0.8.1",
"postcss": "8.4.35", "postcss": "8.4.35",
"pusher-js": "8.4.0-rc2", "pusher-js": "8.4.0-rc2",
"tailwindcss": "3.4.1", "tailwindcss": "3.4.1",
"vite": "4.5.2", "vite": "4.5.2",
"vue": "3.4.19" "vue": "3.4.21"
}, },
"dependencies": { "dependencies": {
"@tailwindcss/typography": "0.5.10", "@tailwindcss/typography": "0.5.10",

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

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

@@ -1,8 +1,10 @@
<div class="group"> <div class="group">
@if (data_get($application, 'fqdn') || @if (
(data_get($application, 'fqdn') ||
collect(json_decode($this->application->docker_compose_domains))->count() > 0 || collect(json_decode($this->application->docker_compose_domains))->count() > 0 ||
data_get($application, 'previews', collect([]))->count() > 0 || data_get($application, 'previews', collect([]))->count() > 0 ||
data_get($application, 'ports_mappings_array')) data_get($application, 'ports_mappings_array')) &&
data_get($application, 'settings.is_raw_compose_deployment_enabled') !== true)
<label tabindex="0" class="flex items-center gap-2 cursor-pointer hover:text-white"> Open Application <label tabindex="0" class="flex items-center gap-2 cursor-pointer hover:text-white"> Open Application
<x-chevron-down /> <x-chevron-down />
</label> </label>

View File

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

View File

@@ -5,27 +5,6 @@
</a> </a>
<x-services.links /> <x-services.links />
<div class="flex-1"></div> <div class="flex-1"></div>
@if (str($service->status())->contains('degraded'))
<button wire:click='deploy' onclick="startService.showModal()"
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg class="w-5 h-5 text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
<path d="M20 4v5h-5" />
</g>
</svg>
Restart Degraded Services
</button>
<button wire:click='stop' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
<path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
</svg>
Stop
</button>
@endif
@if (str($service->status())->contains('running')) @if (str($service->status())->contains('running'))
<button wire:click='restart' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400"> <button wire:click='restart' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg class="w-5 h-5 text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg class="w-5 h-5 text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
@@ -45,8 +24,27 @@
</svg> </svg>
Stop Stop
</button> </button>
@endif @elseif (str($service->status())->contains('degraded'))
@if (str($service->status())->contains('exited')) <button wire:click='deploy' onclick="startService.showModal()"
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg class="w-5 h-5 text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
<path d="M20 4v5h-5" />
</g>
</svg>
Restart Degraded Services
</button>
<button wire:click='stop' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
<path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
</svg>
Stop
</button>
@elseif (str($service->status())->contains('exited'))
<button wire:click='stop(true)' <button wire:click='stop(true)'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400"> class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg class="w-5 h-5 " viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"> <svg class="w-5 h-5 " viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
@@ -65,6 +63,25 @@
</svg> </svg>
Deploy Deploy
</button> </button>
@else
<button wire:click='stop' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
<path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
</svg>
Stop
</button>
<button wire:click='deploy' onclick="startService.showModal()"
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-warning" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
Deploy
</button>
@endif @endif
</div> </div>

View File

@@ -1,8 +1,8 @@
@if (Str::of($complexStatus)->startsWith('running')) @if (str($complexStatus)->contains('running'))
<x-status.running :status="$complexStatus" /> <x-status.running :status="$complexStatus" />
@elseif(Str::of($complexStatus)->startsWith('restarting')) @elseif(str($complexStatus)->contains('restarting'))
<x-status.restarting :status="$complexStatus" /> <x-status.restarting :status="$complexStatus" />
@elseif(Str::of($complexStatus)->startsWith('degraded')) @elseif(str($complexStatus)->contains('degraded'))
<x-status.degraded :status="$complexStatus" /> <x-status.degraded :status="$complexStatus" />
@else @else
<x-status.stopped :status="$complexStatus" /> <x-status.stopped :status="$complexStatus" />

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

@@ -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

@@ -5,6 +5,9 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preconnect" href="https://api.fonts.coollabs.io" crossorigin> <link rel="preconnect" href="https://api.fonts.coollabs.io" crossorigin>
<link rel="dns-prefetch" href="https://api.fonts.coollabs.io" />
<link rel="preload" href="https://api.fonts.coollabs.io/css2?family=Inter&display=swap" as="style" />
<link rel="preload" href="https://cdn.fonts.coollabs.io/inter/normal/400.woff2" as="style" />
<link href="https://api.fonts.coollabs.io/css2?family=Inter&display=swap" rel="stylesheet"> <link href="https://api.fonts.coollabs.io/css2?family=Inter&display=swap" rel="stylesheet">
<meta name="robots" content="noindex"> <meta name="robots" content="noindex">
<title>Coolify</title> <title>Coolify</title>

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

@@ -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,57 @@
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> <x-forms.checkbox label="Enable gzip compression"
helper="You can disable gzip compression if you want. Some services are compressing data by default. In this case, you do not need this."
instantSave id="is_gzip_enabled" />
<x-forms.checkbox helper="Strip Prefix is used to remove prefixes from paths. Like /api/ to /api."
instantSave id="is_stripprefix_enabled" label="Strip Prefixes" />
<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

@@ -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
@@ -44,9 +45,11 @@
</div> </div>
@endif @endif
@if ($application->build_pack === 'dockercompose') @if ($application->build_pack === 'dockercompose')
<div class="w-96">
<x-forms.checkbox instantSave id="application.settings.is_raw_compose_deployment_enabled" <x-forms.checkbox instantSave id="application.settings.is_raw_compose_deployment_enabled"
label="Raw Compose Deployment" label="Raw Compose Deployment"
helper="WARNING: Advanced use cases only. Your docker compose file will be deployed as-is. Nothing is modified by Coolify. You need to configure the proxy parts. More info in the <a href='https://coolify.io/docs/docker/compose#raw-docker-compose-deployment'>documentation.</a>" /> helper="WARNING: Advanced use cases only. Your docker compose file will be deployed as-is. Nothing is modified by Coolify. You need to configure the proxy parts. More info in the <a href='https://coolify.io/docs/docker/compose#raw-docker-compose-deployment'>documentation.</a>" />
</div>
@if (count($parsedServices) > 0 && !$application->settings->is_raw_compose_deployment_enabled) @if (count($parsedServices) > 0 && !$application->settings->is_raw_compose_deployment_enabled)
@foreach (data_get($parsedServices, 'services') as $serviceName => $service) @foreach (data_get($parsedServices, 'services') as $serviceName => $service)
@if (!isDatabaseImage(data_get($service, 'image'))) @if (!isDatabaseImage(data_get($service, 'image')))
@@ -100,11 +103,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."
@@ -202,7 +209,11 @@
placeholder="--cap-add SYS_ADMIN --device=/dev/fuse --security-opt apparmor:unconfined --ulimit nofile=1024:1024 --tmpfs /run:rw,noexec,nosuid,size=65536k" placeholder="--cap-add SYS_ADMIN --device=/dev/fuse --security-opt apparmor:unconfined --ulimit nofile=1024:1024 --tmpfs /run:rw,noexec,nosuid,size=65536k"
id="application.custom_docker_run_options" label="Custom Docker Options" /> id="application.custom_docker_run_options" label="Custom Docker Options" />
@endif @endif
@else
<x-forms.input
helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='text-white underline' href='https://coolify.io/docs/custom-docker-options'>docs.</a>"
placeholder="--cap-add SYS_ADMIN --device=/dev/fuse --security-opt apparmor:unconfined --ulimit nofile=1024:1024 --tmpfs /run:rw,noexec,nosuid,size=65536k"
id="application.custom_docker_run_options" label="Custom Docker Options" />
@endif @endif
@if ($application->build_pack === 'dockercompose') @if ($application->build_pack === 'dockercompose')
<x-forms.button wire:click="loadComposeFile">Reload Compose File</x-forms.button> <x-forms.button wire:click="loadComposeFile">Reload Compose File</x-forms.button>

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

@@ -12,12 +12,23 @@
</div> </div>
@if (!$branch_found) @if (!$branch_found)
<div class="px-2 pt-4"> <div class="px-2 pt-4">
<p>Public repositories: <span class='text-helper'>https://...</span></p> <div class="flex gap-1">
<p>Private repositories: <span class='text-helper'>git@...</span></p> <div>Public:</div>
<p>Preselect branch: <span <div class='text-helper'>https://..</div>
class='text-helper'>https://github.com/coollabsio/coolify-examples/tree/static</span> to </div>
select 'static' branch.</p> <div class="flex gap-1">
</div> <div>Private:</div>
<div class='text-helper'>git@..</div>
</div>
<div class="flex gap-1">
<div>Preselect branch (eg: static):</div>
<div class='text-helper'>https://github.com/coollabsio/coolify-examples/tree/static</div>
</div>
<div>
For example application deployments, checkout <a class="text-white underline"
href="https://github.com/coollabsio/coolify-examples/" target="_blank">Coolify
Examples</a>.
</div>
@endif @endif
@if ($branch_found) @if ($branch_found)
@if ($rate_limit_remaining && $rate_limit_reset) @if ($rate_limit_remaining && $rate_limit_reset)

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

@@ -34,7 +34,9 @@
<h3 class="pt-2">Advanced</h3> <h3 class="pt-2">Advanced</h3>
<div class="w-96"> <div class="w-96">
<x-forms.checkbox instantSave id="application.is_gzip_enabled" label="Enable gzip compression" <x-forms.checkbox instantSave id="application.is_gzip_enabled" label="Enable gzip compression"
helper="You can disable gzip compression if you want. Some services are compressing data by default. In this case, you do not need this." /> helper="You can disable gzip compression if you want. Some services are compressing data by default. In this case, you do not need this." />
<x-forms.checkbox instantSave id="application.is_stripprefix_enabled" label="Strip Prefixes"
helper="Strip Prefix is used to remove prefixes from paths. Like /api/ to /api." />
<x-forms.checkbox instantSave label="Exclude from service status" <x-forms.checkbox instantSave label="Exclude from service status"
helper="If you do not need to monitor this resource, enable. Useful if this service is optional." helper="If you do not need to monitor this resource, enable. Useful if this service is optional."
id="application.exclude_from_status"></x-forms.checkbox> id="application.exclude_from_status"></x-forms.checkbox>

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

@@ -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>

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