Compare commits

...

36 Commits

Author SHA1 Message Date
Andras Bacsai
a7df9fa625 Merge pull request #1798 from coollabsio/next
v4.0.0-beta.231
2024-03-02 15:04:48 +01:00
Andras Bacsai
9064aedc89 Fix server reference in ExecuteContainerCommand.php 2024-03-02 15:02:55 +01:00
Andras Bacsai
fda5d23d32 feat: logs and execute commands with several servers 2024-03-02 14:55:39 +01:00
Andras Bacsai
60be51dbe0 Update pull_request_id comparison in ApplicationDeploymentJob.php and update version numbers 2024-03-02 13:22:05 +01:00
Andras Bacsai
e9f451339f Merge pull request #1796 from coollabsio/next
fix: unmanaged containers method
2024-03-01 19:14:18 +01:00
Andras Bacsai
4d8ffd05a9 Refactor unmanagedContainers property in Resources.php and add conditional return in loadUnmanagedContainers() method 2024-03-01 19:13:22 +01:00
Andras Bacsai
b630105572 Merge pull request #1795 from coollabsio/next
v4.0.0-beta.230
2024-03-01 19:09:09 +01:00
Andras Bacsai
9fa71f847f Refactor notification channels based on cloud environment 2024-03-01 19:08:00 +01:00
Andras Bacsai
f70a9c6974 Fix notification channels in ApplicationDeploymentJob and DeploymentSuccess 2024-03-01 19:07:21 +01:00
Andras Bacsai
a4d173c733 Fix unmanagedContainers type declaration 2024-03-01 19:00:45 +01:00
Andras Bacsai
2eb7712e09 fix: remove success application deployment job
wip: daily backup status
2024-03-01 18:24:14 +01:00
Andras Bacsai
54923b7640 feat: collect webhooks during maintenance 2024-03-01 14:04:29 +01:00
Andras Bacsai
bb927505fe Merge pull request #1793 from coollabsio/next
v4.0.0-beta.229
2024-03-01 11:47:14 +01:00
Andras Bacsai
5e66e314d2 Update version numbers to 4.0.0-beta.229 2024-03-01 11:44:01 +01:00
Andras Bacsai
6fe791c1f1 fix: pull request deployments + build servers 2024-03-01 11:43:42 +01:00
Andras Bacsai
860c537f81 Add server limit override for development environment 2024-03-01 11:41:28 +01:00
Andras Bacsai
a352e4cbf7 fix: public prs should not be commented 2024-03-01 11:41:22 +01:00
Andras Bacsai
5322d446bd fix: service container status updates 2024-03-01 10:36:32 +01:00
Andras Bacsai
604ab0afd8 Add autofocus to search input field 2024-03-01 10:06:59 +01:00
Andras Bacsai
3d87a88d3d Merge pull request #1790 from coollabsio/next
v4.0.0-beta.228
2024-03-01 09:32:19 +01:00
Andras Bacsai
10f9e22a8e fix: do not show n/a networsk 2024-03-01 09:28:14 +01:00
Andras Bacsai
8edda0cdda fix: load unmanaged async 2024-03-01 09:25:27 +01:00
Andras Bacsai
21047afc02 Add new sponsor image 2024-03-01 09:19:23 +01:00
Andras Bacsai
2e9793ffb2 Refactor code for improved performance and readability 2024-02-29 09:21:02 +01:00
Andras Bacsai
fcd100df39 Fix typos and grammatical errors in email templates and form view 2024-02-29 09:16:02 +01:00
Andras Bacsai
dfd564a3a4 Add Supabase logo and update environment variable in compose file 2024-02-29 09:15:06 +01:00
Andras Bacsai
a43c916009 Refactor code and add new fields for Kong service 2024-02-28 13:48:39 +01:00
Andras Bacsai
c8332ca9bf fix: resource tab not loading if server is not reachable 2024-02-28 09:51:45 +01:00
Andras Bacsai
e98170f921 Update Github Sponsors to $40+ 2024-02-28 09:38:59 +01:00
Andras Bacsai
b8f25406cd Refactor code to improve performance and readability 2024-02-27 15:44:19 +01:00
Andras Bacsai
76dcc12b13 Update version numbers to 4.0.0-beta.228 2024-02-27 15:13:30 +01:00
Andras Bacsai
baa2228c9b Merge pull request #1786 from coollabsio/next
v4.0.0-beta.226
2024-02-27 09:10:31 +01:00
Andras Bacsai
5275ae8e9c Refactor getLogs method and update view template 2024-02-27 09:08:15 +01:00
Andras Bacsai
c71e1e107e Refactor getLogs method and update get-logs.blade.php view 2024-02-27 09:05:28 +01:00
Andras Bacsai
8ab72c7e10 feat: preview deployment logs 2024-02-27 09:01:19 +01:00
Andras Bacsai
a8970df91b Update class names in controllers 2024-02-27 08:03:42 +01:00
69 changed files with 3274 additions and 1455 deletions

View File

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

View File

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

View File

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

@@ -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,6 @@ class PreventRequestsDuringMaintenance extends Middleware
* @var array<int, string> * @var array<int, string>
*/ */
protected $except = [ protected $except = [
// 'webhooks/*',
]; ];
} }

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;
} }
@@ -427,13 +427,13 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} }
private function deploy_dockerfile_buildpack() private function deploy_dockerfile_buildpack()
{ {
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch} to {$this->server->name}.");
if ($this->use_build_server) { if ($this->use_build_server) {
$this->server = $this->build_server; $this->server = $this->build_server;
} }
if (data_get($this->application, 'dockerfile_location')) { if (data_get($this->application, 'dockerfile_location')) {
$this->dockerfile_location = $this->application->dockerfile_location; $this->dockerfile_location = $this->application->dockerfile_location;
} }
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch} to {$this->server->name}.");
$this->prepare_builder_image(); $this->prepare_builder_image();
$this->check_git_if_build_needed(); $this->check_git_if_build_needed();
$this->set_base_dir(); $this->set_base_dir();
@@ -528,9 +528,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->server = $this->original_server; $this->server = $this->original_server;
} }
$readme = generate_readme_file($this->application->name, $this->application_deployment_queue->updated_at); $readme = generate_readme_file($this->application->name, $this->application_deployment_queue->updated_at);
$composeFileName = "$this->configuration_dir/docker-compose.yml"; if ($this->pull_request_id === 0) {
if ($this->pull_request_id !== 0) { $composeFileName = "$this->configuration_dir/docker-compose.yml";
} else {
$composeFileName = "$this->configuration_dir/docker-compose-pr-{$this->pull_request_id}.yml"; $composeFileName = "$this->configuration_dir/docker-compose-pr-{$this->pull_request_id}.yml";
$this->docker_compose_location = "/docker-compose-pr-{$this->pull_request_id}.yml";
} }
$this->execute_remote_command( $this->execute_remote_command(
[ [
@@ -725,7 +727,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->write_deployment_configurations(); $this->write_deployment_configurations();
$this->server = $this->original_server; $this->server = $this->original_server;
} }
if (count($this->application->ports_mappings_array) > 0 || (bool) $this->application->settings->is_consistent_container_name_enabled) { if (count($this->application->ports_mappings_array) > 0 || (bool) $this->application->settings->is_consistent_container_name_enabled || $this->pull_request_id !== 0) {
$this->application_deployment_queue->addLogEntry("----------------------------------------"); $this->application_deployment_queue->addLogEntry("----------------------------------------");
if (count($this->application->ports_mappings_array) > 0) { if (count($this->application->ports_mappings_array) > 0) {
$this->application_deployment_queue->addLogEntry("Application has ports mapped to the host system, rolling update is not supported."); $this->application_deployment_queue->addLogEntry("Application has ports mapped to the host system, rolling update is not supported.");
@@ -733,6 +735,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ((bool) $this->application->settings->is_consistent_container_name_enabled) { if ((bool) $this->application->settings->is_consistent_container_name_enabled) {
$this->application_deployment_queue->addLogEntry("Consistent container name feature enabled, rolling update is not supported."); $this->application_deployment_queue->addLogEntry("Consistent container name feature enabled, rolling update is not supported.");
} }
if ($this->pull_request_id !== 0) {
$this->application->settings->is_consistent_container_name_enabled = true;
$this->application_deployment_queue->addLogEntry("Pull request deployment, rolling update is not supported.");
}
$this->stop_running_container(force: true); $this->stop_running_container(force: true);
$this->start_by_compose_file(); $this->start_by_compose_file();
} else { } else {
@@ -810,26 +816,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->add_build_env_variables_to_dockerfile(); $this->add_build_env_variables_to_dockerfile();
} }
$this->build_image(); $this->build_image();
$this->stop_running_container(); $this->push_to_docker_registry();
if ($this->application->destination->server->isSwarm()) { // $this->stop_running_container();
$this->push_to_docker_registry(); $this->rolling_update();
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "docker stack deploy --with-registry-auth -c {$this->workdir}{$this->docker_compose_location} {$this->application->uuid}-{$this->pull_request_id}")
],
);
} else {
$this->application_deployment_queue->addLogEntry("Starting preview deployment.");
if ($this->use_build_server) {
$this->execute_remote_command(
["SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->configuration_dir} -f {$this->configuration_dir}{$this->docker_compose_location} up --build -d", "hidden" => true],
);
} else {
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up --build -d"), "hidden" => true],
);
}
}
} }
private function create_workdir() private function create_workdir()
{ {
@@ -1226,43 +1215,45 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
// ]; // ];
// } // }
if ((bool)$this->application->settings->is_consistent_container_name_enabled) { if ($this->pull_request_id === 0) {
$custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options); if ((bool)$this->application->settings->is_consistent_container_name_enabled) {
if (count($custom_compose) > 0) { $custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options);
$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 +1530,18 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id); $containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id);
if ($this->pull_request_id === 0) { if ($this->pull_request_id === 0) {
$containers = $containers->filter(function ($container) { $containers = $containers->filter(function ($container) {
return data_get($container, 'Names') !== $this->container_name; return data_get($container, 'Names') !== $this->container_name && data_get($container, 'Names') !== $this->container_name . '-pr-' . $this->pull_request_id;
}); });
} }
$containers->each(function ($container) { $containers->each(function ($container) {
$containerName = data_get($container, 'Names'); $containerName = data_get($container, 'Names');
$this->execute_remote_command( $this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "docker rm -f $containerName >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true], ["docker rm -f $containerName >/dev/null 2>&1", "hidden" => true, "ignore_errors" => true],
); );
}); });
if ($this->application->settings->is_consistent_container_name_enabled) { if ($this->application->settings->is_consistent_container_name_enabled) {
$this->execute_remote_command( $this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true], ["docker rm -f $this->container_name >/dev/null 2>&1", "hidden" => true, "ignore_errors" => true],
); );
} }
} else { } else {
@@ -1559,7 +1550,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
'status' => ApplicationDeploymentStatus::FAILED->value, 'status' => ApplicationDeploymentStatus::FAILED->value,
]); ]);
$this->execute_remote_command( $this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true], ["docker rm -f $this->container_name >/dev/null 2>&1", "hidden" => true, "ignore_errors" => true],
); );
} }
} }
@@ -1680,7 +1671,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

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

View File

@@ -0,0 +1,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

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

View File

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

View File

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

View File

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

View File

@@ -17,7 +17,20 @@ class Navbar extends Component
public array $parameters; public array $parameters;
public array $query; public array $query;
public $isDeploymentProgress = false; public $isDeploymentProgress = false;
public function getListeners()
{
return [
"serviceStatusChanged"
];
}
public function serviceStatusChanged()
{
$this->dispatch('refresh')->self();
}
public function render()
{
return view('livewire.project.service.navbar');
}
public function checkDeployments() public function checkDeployments()
{ {
$activity = Activity::where('properties->type_uuid', $this->service->uuid)->latest()->first(); $activity = Activity::where('properties->type_uuid', $this->service->uuid)->latest()->first();
@@ -28,26 +41,6 @@ class Navbar extends Component
$this->isDeploymentProgress = false; $this->isDeploymentProgress = false;
} }
} }
public function getListeners()
{
return [
"serviceStatusChanged"
];
}
public function serviceStatusChanged()
{
$this->service->refresh();
}
public function render()
{
return view('livewire.project.service.navbar');
}
public function check_status($showNotification = false)
{
dispatch_sync(new ContainerStatusJob($this->service->destination->server));
$this->service->refresh();
if ($showNotification) $this->dispatch('success', 'Service status updated.');
}
public function deploy() public function deploy()
{ {
$this->checkDeployments(); $this->checkDeployments();
@@ -62,7 +55,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

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

View File

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

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

View File

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

View File

@@ -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()
{ {

View File

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

View File

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

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

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

View File

@@ -8,18 +8,21 @@ use Illuminate\Support\Collection;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Spatie\Url\Url; use Spatie\Url\Url;
function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pullRequestId = null): Collection function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pullRequestId = null, ?bool $includePullrequests = false): Collection
{ {
$containers = collect([]); $containers = collect([]);
if (!$server->isSwarm()) { if (!$server->isSwarm()) {
$containers = instant_remote_process(["docker ps -a --filter='label=coolify.applicationId={$id}' --format '{{json .}}' "], $server); $containers = instant_remote_process(["docker ps -a --filter='label=coolify.applicationId={$id}' --format '{{json .}}' "], $server);
$containers = format_docker_command_output_to_json($containers); $containers = format_docker_command_output_to_json($containers);
$containers = $containers->map(function ($container) use ($pullRequestId) { $containers = $containers->map(function ($container) use ($pullRequestId, $includePullrequests) {
$labels = data_get($container, 'Labels'); $labels = data_get($container, 'Labels');
if (!str($labels)->contains("coolify.pullRequestId=")) { if (!str($labels)->contains("coolify.pullRequestId=")) {
data_set($container, 'Labels', $labels . ",coolify.pullRequestId={$pullRequestId}"); data_set($container, 'Labels', $labels . ",coolify.pullRequestId={$pullRequestId}");
return $container; return $container;
} }
if ($includePullrequests) {
return $container;
}
if (str($labels)->contains("coolify.pullRequestId=$pullRequestId")) { if (str($labels)->contains("coolify.pullRequestId=$pullRequestId")) {
return $container; return $container;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

After

Width:  |  Height:  |  Size: 203 B

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

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

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
<?php <?php
use App\Http\Controllers\Api\APIDeploy as Deploy; use App\Http\Controllers\Api\Deploy;
use App\Http\Controllers\Api\APIProject as Project; use App\Http\Controllers\Api\Project;
use App\Http\Controllers\Api\APIServer as Server; use App\Http\Controllers\Api\Server;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;

File diff suppressed because it is too large Load Diff

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

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