mirror of
https://github.com/ershisan99/coolify.git
synced 2025-12-24 20:49:32 +00:00
Compare commits
76 Commits
v4.0.0-bet
...
v4.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7df9fa625 | ||
|
|
9064aedc89 | ||
|
|
fda5d23d32 | ||
|
|
60be51dbe0 | ||
|
|
e9f451339f | ||
|
|
4d8ffd05a9 | ||
|
|
b630105572 | ||
|
|
9fa71f847f | ||
|
|
f70a9c6974 | ||
|
|
a4d173c733 | ||
|
|
2eb7712e09 | ||
|
|
54923b7640 | ||
|
|
bb927505fe | ||
|
|
5e66e314d2 | ||
|
|
6fe791c1f1 | ||
|
|
860c537f81 | ||
|
|
a352e4cbf7 | ||
|
|
5322d446bd | ||
|
|
604ab0afd8 | ||
|
|
3d87a88d3d | ||
|
|
10f9e22a8e | ||
|
|
8edda0cdda | ||
|
|
21047afc02 | ||
|
|
2e9793ffb2 | ||
|
|
fcd100df39 | ||
|
|
dfd564a3a4 | ||
|
|
a43c916009 | ||
|
|
c8332ca9bf | ||
|
|
e98170f921 | ||
|
|
b8f25406cd | ||
|
|
76dcc12b13 | ||
|
|
baa2228c9b | ||
|
|
5275ae8e9c | ||
|
|
c71e1e107e | ||
|
|
8ab72c7e10 | ||
|
|
a8970df91b | ||
|
|
2468251f56 | ||
|
|
6e74f3e40e | ||
|
|
407f84a4bb | ||
|
|
91632f0adb | ||
|
|
af3c575d84 | ||
|
|
bf1475441d | ||
|
|
9268f9db1d | ||
|
|
600c43827a | ||
|
|
74092ea95b | ||
|
|
b67abe58e8 | ||
|
|
678647f39a | ||
|
|
453956172b | ||
|
|
b550c32f9b | ||
|
|
f6b886adbc | ||
|
|
9642453052 | ||
|
|
64fca99c26 | ||
|
|
c7da43f50d | ||
|
|
6efa2dd9ba | ||
|
|
5e980c5fe0 | ||
|
|
c8c7a415ea | ||
|
|
c3cfb8d23b | ||
|
|
1b055f0316 | ||
|
|
1fcbf0b363 | ||
|
|
61dbc81765 | ||
|
|
b8b76dfa40 | ||
|
|
297b314904 | ||
|
|
55dd1ab0a1 | ||
|
|
8c803f1c4b | ||
|
|
3b942049a2 | ||
|
|
f78fd212bb | ||
|
|
f931ebece8 | ||
|
|
ea0a9763bf | ||
|
|
b59e47dcf9 | ||
|
|
ce09ef8848 | ||
|
|
188727daba | ||
|
|
1150633fef | ||
|
|
62ae845f4b | ||
|
|
0757fd741e | ||
|
|
a07fa8ccd2 | ||
|
|
c77f32e696 |
@@ -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://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://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>
|
||||
|
||||
@@ -16,7 +16,7 @@ class StartService
|
||||
$commands[] = "cd " . $service->workdir();
|
||||
$commands[] = "echo 'Saved configuration files to {$service->workdir()}.'";
|
||||
$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 'Pulling images.'";
|
||||
$commands[] = "docker compose pull";
|
||||
|
||||
25
app/Console/Commands/CleanupApplicationDeploymentQueue.php
Normal file
25
app/Console/Commands/CleanupApplicationDeploymentQueue.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class CleanupApplicationDeploymentQueue extends Command
|
||||
{
|
||||
protected $signature = 'cleanup:application-deployment-queue {--team-id=}';
|
||||
protected $description = 'CleanupApplicationDeploymentQueue';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$team_id = $this->option('team-id');
|
||||
$servers = \App\Models\Server::where('team_id', $team_id)->get();
|
||||
foreach ($servers as $server) {
|
||||
$deployments = ApplicationDeploymentQueue::whereIn("status", ["in_progress", "queued"])->where("server_id", $server->id)->get();
|
||||
foreach ($deployments as $deployment) {
|
||||
$deployment->update(['status' => 'failed']);
|
||||
instant_remote_process(['docker rm -f ' . $deployment->deployment_uuid], $server, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
59
app/Console/Commands/CleanupDatabase.php
Normal file
59
app/Console/Commands/CleanupDatabase.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class CleanupDatabase extends Command
|
||||
{
|
||||
protected $signature = 'cleanup:database {--yes}';
|
||||
protected $description = 'Cleanup database';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
echo "Running database cleanup...\n";
|
||||
$keep_days = 60;
|
||||
|
||||
// Cleanup failed jobs table
|
||||
$failed_jobs = DB::table('failed_jobs')->where('failed_at', '<', now()->subDays(7));
|
||||
$count = $failed_jobs->count();
|
||||
echo "Delete $count entries from failed_jobs.\n";
|
||||
if ($this->option('yes')) {
|
||||
$failed_jobs->delete();
|
||||
}
|
||||
|
||||
// Cleanup sessions table
|
||||
$sessions = DB::table('sessions')->where('last_activity', '<', now()->subDays($keep_days)->timestamp);
|
||||
$count = $sessions->count();
|
||||
echo "Delete $count entries from sessions.\n";
|
||||
if ($this->option('yes')) {
|
||||
$sessions->delete();
|
||||
}
|
||||
|
||||
// Cleanup activity_log table
|
||||
$activity_log = DB::table('activity_log')->where('created_at', '<', now()->subDays($keep_days));
|
||||
$count = $activity_log->count();
|
||||
echo "Delete $count entries from activity_log.\n";
|
||||
if ($this->option('yes')) {
|
||||
$activity_log->delete();
|
||||
}
|
||||
|
||||
// Cleanup application_deployment_queues table
|
||||
$application_deployment_queues = DB::table('application_deployment_queues')->where('created_at', '<', now()->subDays($keep_days));
|
||||
$count = $application_deployment_queues->count();
|
||||
echo "Delete $count entries from application_deployment_queues.\n";
|
||||
if ($this->option('yes')) {
|
||||
$application_deployment_queues->delete();
|
||||
}
|
||||
|
||||
// Cleanup webhooks table
|
||||
$webhooks = DB::table('webhooks')->where('created_at', '<', now()->subDays($keep_days));
|
||||
$count = $webhooks->count();
|
||||
echo "Delete $count entries from webhooks.\n";
|
||||
if ($this->option('yes')) {
|
||||
$webhooks->delete();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -8,15 +8,16 @@ use Illuminate\Console\Command;
|
||||
class CleanupUnreachableServers extends Command
|
||||
{
|
||||
protected $signature = 'cleanup:unreachable-servers';
|
||||
protected $description = 'Cleanup Unreachable Servers (3 days)';
|
||||
protected $description = 'Cleanup Unreachable Servers (7 days)';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
echo "Running unreachable server cleanup...\n";
|
||||
$servers = Server::where('unreachable_count', 3)->where('unreachable_notification_sent', true)->where('updated_at', '<', now()->subDays(3))->get();
|
||||
$servers = Server::where('unreachable_count', 3)->where('unreachable_notification_sent', true)->where('updated_at', '<', now()->subDays(7))->get();
|
||||
if ($servers->count() > 0) {
|
||||
foreach ($servers as $server) {
|
||||
echo "Cleanup unreachable server ($server->id) with name $server->name";
|
||||
send_internal_notification("Server $server->name is unreachable for 7 days. Cleaning up...");
|
||||
$server->update([
|
||||
'ip' => '1.2.3.4'
|
||||
]);
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Jobs\DatabaseBackupStatusJob;
|
||||
use App\Jobs\SendConfirmationForWaitlistJob;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Models\ScheduledDatabaseBackupExecution;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\Team;
|
||||
@@ -15,6 +17,7 @@ use App\Notifications\Application\DeploymentSuccess;
|
||||
use App\Notifications\Application\StatusChanged;
|
||||
use App\Notifications\Database\BackupFailed;
|
||||
use App\Notifications\Database\BackupSuccess;
|
||||
use App\Notifications\Database\DailyBackup;
|
||||
use App\Notifications\Test;
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
@@ -54,6 +57,8 @@ class Emails extends Command
|
||||
options: [
|
||||
'updates' => 'Send Update Email to all users',
|
||||
'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-failed' => 'Application - Deployment Failed',
|
||||
'application-status-changed' => 'Application - Status Changed',
|
||||
@@ -67,8 +72,12 @@ class Emails extends Command
|
||||
],
|
||||
);
|
||||
$emailsGathered = ['realusers-before-trial', 'realusers-server-lost-connection'];
|
||||
if (!in_array($type, $emailsGathered)) {
|
||||
$this->email = text('Email Address to send to');
|
||||
if (isDev()) {
|
||||
$this->email = "test@example.com";
|
||||
} else {
|
||||
if (!in_array($type, $emailsGathered)) {
|
||||
$this->email = text('Email Address to send to:');
|
||||
}
|
||||
}
|
||||
set_transanctional_email_settings();
|
||||
|
||||
@@ -102,7 +111,7 @@ class Emails extends Command
|
||||
$unsubscribeUrl = route('unsubscribe.marketing.emails', [
|
||||
'token' => encrypt($email),
|
||||
]);
|
||||
$this->mail->view('emails.updates',["unsubscribeUrl" => $unsubscribeUrl]);
|
||||
$this->mail->view('emails.updates', ["unsubscribeUrl" => $unsubscribeUrl]);
|
||||
$this->sendEmail($email);
|
||||
}
|
||||
}
|
||||
@@ -111,6 +120,35 @@ class Emails extends Command
|
||||
$this->mail = (new Test())->toMail();
|
||||
$this->sendEmail();
|
||||
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':
|
||||
$application = Application::all()->first();
|
||||
$this->mail = (new DeploymentSuccess($application, 'test'))->toMail();
|
||||
|
||||
@@ -69,12 +69,34 @@ class Kernel extends ConsoleKernel
|
||||
}
|
||||
foreach ($containerServers as $server) {
|
||||
$schedule->job(new ContainerStatusJob($server))->everyMinute()->onOneServer();
|
||||
// $schedule
|
||||
// ->call(function () use ($server) {
|
||||
// $randomSeconds = rand(1, 40);
|
||||
// $job = new ContainerStatusJob($server);
|
||||
// $job->delay($randomSeconds);
|
||||
// ray('dispatching container status job in ' . $randomSeconds . ' seconds');
|
||||
// dispatch($job);
|
||||
// })->name('container-status-' . $server->id)->everyMinute()->onOneServer();
|
||||
if ($server->isLogDrainEnabled()) {
|
||||
$schedule->job(new CheckLogDrainContainerJob($server))->everyMinute()->onOneServer();
|
||||
// $schedule
|
||||
// ->call(function () use ($server) {
|
||||
// $randomSeconds = rand(1, 40);
|
||||
// $job = new CheckLogDrainContainerJob($server);
|
||||
// $job->delay($randomSeconds);
|
||||
// dispatch($job);
|
||||
// })->name('log-drain-container-check-' . $server->id)->everyMinute()->onOneServer();
|
||||
}
|
||||
}
|
||||
foreach ($servers as $server) {
|
||||
$schedule->job(new ServerStatusJob($server))->everyMinute()->onOneServer();
|
||||
// $schedule
|
||||
// ->call(function () use ($server) {
|
||||
// $randomSeconds = rand(1, 40);
|
||||
// $job = new ServerStatusJob($server);
|
||||
// $job->delay($randomSeconds);
|
||||
// dispatch($job);
|
||||
// })->name('server-status-job-' . $server->id)->everyMinute()->onOneServer();
|
||||
}
|
||||
}
|
||||
private function instance_auto_update($schedule)
|
||||
|
||||
186
app/Http/Controllers/Webhook/Bitbucket.php
Normal file
186
app/Http/Controllers/Webhook/Bitbucket.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
459
app/Http/Controllers/Webhook/Github.php
Normal file
459
app/Http/Controllers/Webhook/Github.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
202
app/Http/Controllers/Webhook/Gitlab.php
Normal file
202
app/Http/Controllers/Webhook/Gitlab.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
258
app/Http/Controllers/Webhook/Stripe.php
Normal file
258
app/Http/Controllers/Webhook/Stripe.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
58
app/Http/Controllers/Webhook/Waitlist.php
Normal file
58
app/Http/Controllers/Webhook/Waitlist.php
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,7 @@ class DecideWhatToDoWithUser
|
||||
if (auth()->user()->hasVerifiedEmail() && $request->path() === 'verify') {
|
||||
return redirect(RouteServiceProvider::HOME);
|
||||
}
|
||||
if (isSubscriptionActive() && $request->path() === 'subscription') {
|
||||
if (isSubscriptionActive() && $request->routeIs('subscription.index')) {
|
||||
return redirect(RouteServiceProvider::HOME);
|
||||
}
|
||||
return $next($request);
|
||||
|
||||
@@ -12,6 +12,6 @@ class PreventRequestsDuringMaintenance extends Middleware
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $except = [
|
||||
//
|
||||
'webhooks/*',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -167,65 +167,71 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
if ($this->application->is_github_based()) {
|
||||
ApplicationPullRequestUpdateJob::dispatch(application: $this->application, preview: $this->preview, deployment_uuid: $this->deployment_uuid, status: ProcessStatus::IN_PROGRESS);
|
||||
}
|
||||
if ($this->application->build_pack === 'dockerfile') {
|
||||
if (data_get($this->application, 'dockerfile_location')) {
|
||||
$this->dockerfile_location = $this->application->dockerfile_location;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
// Generate custom host<->ip mapping
|
||||
$allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server);
|
||||
if (!is_null($allContainers)) {
|
||||
$allContainers = format_docker_command_output_to_json($allContainers);
|
||||
$ips = collect([]);
|
||||
if (count($allContainers) > 0) {
|
||||
$allContainers = $allContainers[0];
|
||||
$allContainers = collect($allContainers)->sort()->values();
|
||||
foreach ($allContainers as $container) {
|
||||
$containerName = data_get($container, 'Name');
|
||||
if ($containerName === 'coolify-proxy') {
|
||||
continue;
|
||||
}
|
||||
if (preg_match('/-(\d{12})/', $containerName)) {
|
||||
continue;
|
||||
}
|
||||
$containerIp = data_get($container, 'IPv4Address');
|
||||
if ($containerName && $containerIp) {
|
||||
$containerIp = str($containerIp)->before('/');
|
||||
$ips->put($containerName, $containerIp->value());
|
||||
try {
|
||||
// Generate custom host<->ip mapping
|
||||
$allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server);
|
||||
|
||||
if (!is_null($allContainers)) {
|
||||
$allContainers = format_docker_command_output_to_json($allContainers);
|
||||
$ips = collect([]);
|
||||
if (count($allContainers) > 0) {
|
||||
$allContainers = $allContainers[0];
|
||||
$allContainers = collect($allContainers)->sort()->values();
|
||||
foreach ($allContainers as $container) {
|
||||
$containerName = data_get($container, 'Name');
|
||||
if ($containerName === 'coolify-proxy') {
|
||||
continue;
|
||||
}
|
||||
if (preg_match('/-(\d{12})/', $containerName)) {
|
||||
continue;
|
||||
}
|
||||
$containerIp = data_get($container, 'IPv4Address');
|
||||
if ($containerName && $containerIp) {
|
||||
$containerIp = str($containerIp)->before('/');
|
||||
$ips->put($containerName, $containerIp->value());
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->addHosts = $ips->map(function ($ip, $name) {
|
||||
return "--add-host $name:$ip";
|
||||
})->implode(' ');
|
||||
}
|
||||
$this->addHosts = $ips->map(function ($ip, $name) {
|
||||
return "--add-host $name:$ip";
|
||||
})->implode(' ');
|
||||
}
|
||||
|
||||
if ($this->application->dockerfile_target_build) {
|
||||
$this->buildTarget = " --target {$this->application->dockerfile_target_build} ";
|
||||
}
|
||||
if ($this->application->dockerfile_target_build) {
|
||||
$this->buildTarget = " --target {$this->application->dockerfile_target_build} ";
|
||||
}
|
||||
|
||||
// Check custom port
|
||||
['repository' => $this->customRepository, 'port' => $this->customPort] = $this->application->customRepository();
|
||||
// Check custom port
|
||||
['repository' => $this->customRepository, 'port' => $this->customPort] = $this->application->customRepository();
|
||||
|
||||
if (data_get($this->application, 'settings.is_build_server_enabled')) {
|
||||
$teamId = data_get($this->application, 'environment.project.team.id');
|
||||
$buildServers = Server::buildServers($teamId)->get();
|
||||
if ($buildServers->count() === 0) {
|
||||
$this->application_deployment_queue->addLogEntry("Build server feature activated, but no suitable build server found. Using the deployment server.");
|
||||
if (data_get($this->application, 'settings.is_build_server_enabled')) {
|
||||
$teamId = data_get($this->application, 'environment.project.team.id');
|
||||
$buildServers = Server::buildServers($teamId)->get();
|
||||
if ($buildServers->count() === 0) {
|
||||
$this->application_deployment_queue->addLogEntry("No suitable build server found. Using the deployment server.");
|
||||
$this->build_server = $this->server;
|
||||
$this->original_server = $this->server;
|
||||
} else {
|
||||
$this->build_server = $buildServers->random();
|
||||
$this->application_deployment_queue->addLogEntry("Found a suitable build server ({$this->build_server->name}).");
|
||||
$this->original_server = $this->server;
|
||||
$this->use_build_server = true;
|
||||
}
|
||||
} else {
|
||||
// Set build server & original_server to the same as deployment server
|
||||
$this->build_server = $this->server;
|
||||
$this->original_server = $this->server;
|
||||
} else {
|
||||
$this->application_deployment_queue->addLogEntry("Build server feature activated and found a suitable build server. Using it to build your application - if needed.");
|
||||
$this->build_server = $buildServers->random();
|
||||
$this->original_server = $this->server;
|
||||
$this->use_build_server = true;
|
||||
}
|
||||
} else {
|
||||
// Set build server & original_server to the same as deployment server
|
||||
$this->build_server = $this->server;
|
||||
$this->original_server = $this->server;
|
||||
}
|
||||
try {
|
||||
if ($this->restart_only && $this->application->build_pack !== 'dockerimage') {
|
||||
$this->just_restart();
|
||||
if ($this->server->isProxyShouldRun()) {
|
||||
@@ -421,13 +427,13 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
}
|
||||
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) {
|
||||
$this->server = $this->build_server;
|
||||
}
|
||||
if (data_get($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->check_git_if_build_needed();
|
||||
$this->set_base_dir();
|
||||
@@ -522,9 +528,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
$this->server = $this->original_server;
|
||||
}
|
||||
$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";
|
||||
$this->docker_compose_location = "/docker-compose-pr-{$this->pull_request_id}.yml";
|
||||
}
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
@@ -719,7 +727,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
$this->write_deployment_configurations();
|
||||
$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("----------------------------------------");
|
||||
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.");
|
||||
@@ -727,6 +735,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
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.");
|
||||
}
|
||||
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->start_by_compose_file();
|
||||
} else {
|
||||
@@ -804,26 +816,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
$this->add_build_env_variables_to_dockerfile();
|
||||
}
|
||||
$this->build_image();
|
||||
$this->stop_running_container();
|
||||
if ($this->application->destination->server->isSwarm()) {
|
||||
$this->push_to_docker_registry();
|
||||
$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],
|
||||
);
|
||||
}
|
||||
}
|
||||
$this->push_to_docker_registry();
|
||||
// $this->stop_running_container();
|
||||
$this->rolling_update();
|
||||
}
|
||||
private function create_workdir()
|
||||
{
|
||||
@@ -1220,17 +1215,45 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
// ];
|
||||
// }
|
||||
|
||||
if ((bool)$this->application->settings->is_consistent_container_name_enabled) {
|
||||
$custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options);
|
||||
if (count($custom_compose) > 0) {
|
||||
$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) {
|
||||
$docker_compose['services'][$this->application->uuid] = array_merge_recursive($docker_compose['services'][$this->application->uuid], $custom_compose);
|
||||
if ($this->pull_request_id === 0) {
|
||||
if ((bool)$this->application->settings->is_consistent_container_name_enabled) {
|
||||
$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->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);
|
||||
}
|
||||
} else {
|
||||
$docker_compose['services'][$this->application->uuid] = $docker_compose['services'][$this->container_name];
|
||||
data_forget($docker_compose, 'services.' . $this->container_name);
|
||||
$custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options);
|
||||
if (count($custom_compose) > 0) {
|
||||
$ipv4 = data_get($custom_compose, 'ip.0');
|
||||
$ipv6 = data_get($custom_compose, 'ip6.0');
|
||||
data_forget($custom_compose, 'ip');
|
||||
data_forget($custom_compose, 'ip6');
|
||||
if ($ipv4 || $ipv6) {
|
||||
data_forget($docker_compose['services'][$this->application->uuid], 'networks');
|
||||
}
|
||||
if ($ipv4) {
|
||||
$docker_compose['services'][$this->application->uuid]['networks'][$this->destination->network]['ipv4_address'] = $ipv4;
|
||||
}
|
||||
if ($ipv6) {
|
||||
$docker_compose['services'][$this->application->uuid]['networks'][$this->destination->network]['ipv6_address'] = $ipv6;
|
||||
}
|
||||
$docker_compose['services'][$this->application->uuid] = array_merge_recursive($docker_compose['services'][$this->application->uuid], $custom_compose);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1507,18 +1530,18 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
$containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id);
|
||||
if ($this->pull_request_id === 0) {
|
||||
$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) {
|
||||
$containerName = data_get($container, 'Names');
|
||||
$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) {
|
||||
$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 {
|
||||
@@ -1527,7 +1550,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
'status' => ApplicationDeploymentStatus::FAILED->value,
|
||||
]);
|
||||
$this->execute_remote_command(
|
||||
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true],
|
||||
["docker rm -f $this->container_name >/dev/null 2>&1", "hidden" => true, "ignore_errors" => true],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1634,6 +1657,8 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
|
||||
public function failed(Throwable $exception): void
|
||||
{
|
||||
|
||||
$this->next(ApplicationDeploymentStatus::FAILED->value);
|
||||
$this->application_deployment_queue->addLogEntry("Oops something is not okay, are you okay? 😢", 'stderr');
|
||||
if (str($exception->getMessage())->isNotEmpty()) {
|
||||
$this->application_deployment_queue->addLogEntry($exception->getMessage(), 'stderr');
|
||||
@@ -1641,15 +1666,14 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
|
||||
if ($this->application->build_pack !== 'dockercompose') {
|
||||
$code = $exception->getCode();
|
||||
ray($code);
|
||||
if ($code !== 69420) {
|
||||
// 69420 means failed to push the image to the registry, so we don't need to remove the new version as it is the currently running one
|
||||
$this->application_deployment_queue->addLogEntry("Deployment failed. Removing the new version of your application.", 'stderr');
|
||||
$this->execute_remote_command(
|
||||
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true]
|
||||
["docker rm -f $this->container_name >/dev/null 2>&1", "hidden" => true, "ignore_errors" => true]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$this->next(ApplicationDeploymentStatus::FAILED->value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,9 @@ class ApplicationPullRequestUpdateJob implements ShouldQueue, ShouldBeEncrypted
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
if ($this->application->is_public_repository()) {
|
||||
return;
|
||||
}
|
||||
if ($this->status === ProcessStatus::CLOSED) {
|
||||
$this->delete_comment();
|
||||
return;
|
||||
|
||||
@@ -43,6 +43,10 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
|
||||
|
||||
public function handle()
|
||||
{
|
||||
if (!$this->server->isFunctional()) {
|
||||
return 'Server is not ready.';
|
||||
};
|
||||
|
||||
$applications = $this->server->applications();
|
||||
$skip_these_applications = collect([]);
|
||||
foreach ($applications as $application) {
|
||||
@@ -57,10 +61,6 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
|
||||
$applications = $applications->filter(function ($value, $key) use ($skip_these_applications) {
|
||||
return !$skip_these_applications->pluck('id')->contains($value->id);
|
||||
});
|
||||
|
||||
if (!$this->server->isFunctional()) {
|
||||
return 'Server is not ready.';
|
||||
};
|
||||
try {
|
||||
if ($this->server->isSwarm()) {
|
||||
$containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this->server, false);
|
||||
|
||||
74
app/Jobs/DatabaseBackupStatusJob.php
Normal file
74
app/Jobs/DatabaseBackupStatusJob.php
Normal 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));
|
||||
// }
|
||||
}
|
||||
}
|
||||
68
app/Jobs/ServerLimitCheckJob.php
Normal file
68
app/Jobs/ServerLimitCheckJob.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Team;
|
||||
use App\Notifications\Server\ForceDisabled;
|
||||
use App\Notifications\Server\ForceEnabled;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ServerLimitCheckJob implements ShouldQueue, ShouldBeEncrypted
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $tries = 4;
|
||||
public function backoff(): int
|
||||
{
|
||||
return isDev() ? 1 : 3;
|
||||
}
|
||||
public function __construct(public Team $team)
|
||||
{
|
||||
}
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping($this->team->uuid))];
|
||||
}
|
||||
|
||||
public function uniqueId(): int
|
||||
{
|
||||
return $this->team->uuid;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
$servers = $this->team->servers;
|
||||
$servers_count = $servers->count();
|
||||
$limit = $this->team->limits['serverLimit'];
|
||||
$number_of_servers_to_disable = $servers_count - $limit;
|
||||
ray('ServerLimitCheckJob', $this->team->uuid, $servers_count, $limit, $number_of_servers_to_disable);
|
||||
if ($number_of_servers_to_disable > 0) {
|
||||
ray('Disabling servers');
|
||||
$servers = $servers->sortbyDesc('created_at');
|
||||
$servers_to_disable = $servers->take($number_of_servers_to_disable);
|
||||
$servers_to_disable->each(function ($server) {
|
||||
$server->forceDisableServer();
|
||||
$this->team->notify(new ForceDisabled($server));
|
||||
});
|
||||
} else if ($number_of_servers_to_disable === 0) {
|
||||
$servers->each(function ($server) {
|
||||
if ($server->isForceDisabled()) {
|
||||
$server->forceEnableServer();
|
||||
$this->team->notify(new ForceEnabled($server));
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
send_internal_notification('ServerLimitCheckJob failed with: ' . $e->getMessage());
|
||||
ray($e->getMessage());
|
||||
return handleError($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,15 +41,6 @@ class ServerStatusJob implements ShouldQueue, ShouldBeEncrypted
|
||||
throw new \RuntimeException('Server is not ready.');
|
||||
};
|
||||
try {
|
||||
// $this->server->validateConnection();
|
||||
// $this->server->validateOS();
|
||||
// $docker_installed = $this->server->validateDockerEngine();
|
||||
// if (!$docker_installed) {
|
||||
// $this->server->installDocker();
|
||||
// $this->server->validateDockerEngine();
|
||||
// }
|
||||
|
||||
// $this->server->validateDockerEngineVersion();
|
||||
if ($this->server->isFunctional()) {
|
||||
$this->cleanup(notify: false);
|
||||
}
|
||||
|
||||
52
app/Listeners/MaintenanceModeDisabledNotification.php
Normal file
52
app/Listeners/MaintenanceModeDisabledNotification.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
27
app/Listeners/MaintenanceModeEnabledNotification.php
Normal file
27
app/Listeners/MaintenanceModeEnabledNotification.php
Normal 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!');
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Livewire\Admin;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -14,28 +15,26 @@ class Index extends Component
|
||||
if (!isCloud()) {
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
if (auth()->user()->id !== 0 && session('adminToken') === null) {
|
||||
if (auth()->user()->id !== 0) {
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
$this->users = User::whereHas('teams', function ($query) {
|
||||
$query->whereRelation('subscription', 'stripe_subscription_id', '!=', null);
|
||||
})->get();
|
||||
})->get()->filter(function ($user) {
|
||||
return $user->id !== 0;
|
||||
});
|
||||
}
|
||||
public function switchUser(int $user_id)
|
||||
{
|
||||
$user = User::find($user_id);
|
||||
auth()->login($user);
|
||||
|
||||
if ($user_id === 0) {
|
||||
session()->forget('adminToken');
|
||||
} else {
|
||||
$token_payload = [
|
||||
'valid' => true,
|
||||
];
|
||||
$token = Crypt::encrypt($token_payload);
|
||||
session(['adminToken' => $token]);
|
||||
if (auth()->user()->id !== 0) {
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
return refreshSession();
|
||||
$user = User::find($user_id);
|
||||
$team_to_switch_to = $user->teams->first();
|
||||
Cache::forget("team:{$user->id}");
|
||||
auth()->login($user);
|
||||
refreshSession($team_to_switch_to);
|
||||
return redirect(request()->header('Referer'));
|
||||
}
|
||||
public function render()
|
||||
{
|
||||
|
||||
@@ -23,8 +23,8 @@ class Dashboard extends Component
|
||||
public function cleanup_queue()
|
||||
{
|
||||
$this->dispatch('success', 'Cleanup started.');
|
||||
Artisan::queue('app:init', [
|
||||
'--cleanup-deployments' => 'true'
|
||||
Artisan::queue('cleanup:application-deployment-queue', [
|
||||
'--team-id' => currentTeam()->id
|
||||
]);
|
||||
}
|
||||
public function get_deployments()
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
class Sponsorship extends Component
|
||||
class LayoutPopups extends Component
|
||||
{
|
||||
public function getListeners()
|
||||
{
|
||||
@@ -23,6 +23,6 @@ class Sponsorship extends Component
|
||||
}
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.sponsorship');
|
||||
return view('livewire.layout-popups');
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ class Configuration extends Component
|
||||
{
|
||||
public Application $application;
|
||||
public $servers;
|
||||
protected $listeners = ['build_pack_updated' => '$refresh'];
|
||||
protected $listeners = ['buildPackUpdated' => '$refresh'];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
|
||||
@@ -182,7 +182,7 @@ class General extends Component
|
||||
$this->resetDefaultLabels(false);
|
||||
}
|
||||
$this->submit();
|
||||
$this->dispatch('build_pack_updated');
|
||||
$this->dispatch('buildPackUpdated');
|
||||
}
|
||||
public function getWildcardDomain()
|
||||
{
|
||||
|
||||
@@ -29,8 +29,8 @@ class Import extends Component
|
||||
public string $container;
|
||||
public array $importCommands = [];
|
||||
public string $postgresqlRestoreCommand = 'pg_restore -U $POSTGRES_USER -d $POSTGRES_DB';
|
||||
public string $mysqlRestoreCommand = 'mysql -u $MYSQL_USER -p $MYSQL_PASSWORD $MYSQL_DATABASE';
|
||||
public string $mariadbRestoreCommand = 'mariadb -u $MARIADB_USER -p $MARIADB_PASSWORD $MARIADB_DATABASE';
|
||||
public string $mysqlRestoreCommand = 'mysql -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE';
|
||||
public string $mariadbRestoreCommand = 'mariadb -u $MARIADB_USER -p$MARIADB_PASSWORD $MARIADB_DATABASE';
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
|
||||
@@ -10,7 +10,8 @@ use Livewire\Component;
|
||||
class Create extends Component
|
||||
{
|
||||
public $type;
|
||||
public function mount() {
|
||||
public function mount()
|
||||
{
|
||||
$services = getServiceTemplates();
|
||||
$type = str(request()->query('type'));
|
||||
$destination_uuid = request()->query('destination');
|
||||
@@ -70,7 +71,7 @@ class Create extends Component
|
||||
$generatedValue = $value;
|
||||
if ($value->contains('SERVICE_')) {
|
||||
$command = $value->after('SERVICE_')->beforeLast('_');
|
||||
$generatedValue = generateEnvValue($command->value());
|
||||
$generatedValue = generateEnvValue($command->value(), $service);
|
||||
}
|
||||
EnvironmentVariable::create([
|
||||
'key' => $key,
|
||||
|
||||
@@ -17,9 +17,7 @@ class Configuration extends Component
|
||||
{
|
||||
$userId = auth()->user()->id;
|
||||
return [
|
||||
"echo-private:user.{$userId},ServiceStatusChanged" => 'checkStatus',
|
||||
"refreshStacks",
|
||||
"checkStatus",
|
||||
"echo-private:user.{$userId},ServiceStatusChanged" => 'check_status',
|
||||
];
|
||||
}
|
||||
public function render()
|
||||
@@ -37,21 +35,10 @@ class Configuration extends Component
|
||||
$this->applications = $this->service->applications->sort();
|
||||
$this->databases = $this->service->databases->sort();
|
||||
}
|
||||
public function checkStatus()
|
||||
public function check_status()
|
||||
{
|
||||
dispatch_sync(new ContainerStatusJob($this->service->server));
|
||||
$this->refreshStacks();
|
||||
$this->dispatch('refresh')->self();
|
||||
$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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,20 @@ class Navbar extends Component
|
||||
public array $parameters;
|
||||
public array $query;
|
||||
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()
|
||||
{
|
||||
$activity = Activity::where('properties->type_uuid', $this->service->uuid)->latest()->first();
|
||||
@@ -28,26 +41,6 @@ class Navbar extends Component
|
||||
$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()
|
||||
{
|
||||
$this->checkDeployments();
|
||||
@@ -62,9 +55,8 @@ class Navbar extends Component
|
||||
public function stop(bool $forceCleanup = false)
|
||||
{
|
||||
StopService::run($this->service);
|
||||
$this->service->refresh();
|
||||
if ($forceCleanup) {
|
||||
$this->dispatch('success', 'Force cleanup service.');
|
||||
$this->dispatch('success', 'Containers cleaned up.');
|
||||
} else {
|
||||
$this->dispatch('success', 'Service stopped.');
|
||||
}
|
||||
|
||||
@@ -10,29 +10,21 @@ use App\Models\StandaloneMongodb;
|
||||
use App\Models\StandaloneMysql;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\StandaloneRedis;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Component;
|
||||
|
||||
class ExecuteContainerCommand extends Component
|
||||
{
|
||||
public string $command;
|
||||
public string $container;
|
||||
public $containers;
|
||||
public Collection $containers;
|
||||
public $parameters;
|
||||
public $resource;
|
||||
public string $type;
|
||||
public string $workDir = '';
|
||||
public Server $server;
|
||||
public $servers = [];
|
||||
public function getListeners()
|
||||
{
|
||||
return [
|
||||
"serviceStatusChanged",
|
||||
];
|
||||
}
|
||||
public function serviceStatusChanged()
|
||||
{
|
||||
$this->getContainers();
|
||||
}
|
||||
public Collection $servers;
|
||||
|
||||
protected $rules = [
|
||||
'server' => 'required',
|
||||
'container' => 'required',
|
||||
@@ -43,20 +35,18 @@ class ExecuteContainerCommand extends Component
|
||||
public function mount()
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->getContainers();
|
||||
}
|
||||
public function getContainers()
|
||||
{
|
||||
$this->containers = collect();
|
||||
$this->servers = collect();
|
||||
if (data_get($this->parameters, 'application_uuid')) {
|
||||
$this->type = 'application';
|
||||
$this->resource = Application::where('uuid', $this->parameters['application_uuid'])->firstOrFail();
|
||||
$this->server = $this->resource->destination->server;
|
||||
$containers = getCurrentApplicationContainerStatus($this->server, $this->resource->id, 0);
|
||||
if ($containers->count() > 0) {
|
||||
$containers->each(function ($container) {
|
||||
$this->containers->push(str_replace('/', '', $container['Names']));
|
||||
});
|
||||
if ($this->resource->destination->server->isFunctional()) {
|
||||
$this->servers = $this->servers->push($this->resource->destination->server);
|
||||
}
|
||||
foreach ($this->resource->additional_servers as $server) {
|
||||
if ($server->isFunctional()) {
|
||||
$this->servers = $this->servers->push($server);
|
||||
}
|
||||
}
|
||||
} else if (data_get($this->parameters, 'database_uuid')) {
|
||||
$this->type = 'database';
|
||||
@@ -77,44 +67,85 @@ class ExecuteContainerCommand extends Component
|
||||
}
|
||||
}
|
||||
$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;
|
||||
// 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')) {
|
||||
$this->type = 'service';
|
||||
$this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail();
|
||||
$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) {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
if ($this->containers->count() > 0) {
|
||||
$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()
|
||||
{
|
||||
$this->validate();
|
||||
try {
|
||||
// Wrap command to prevent escaped execution in the host.
|
||||
if (data_get($this->parameters, 'application_uuid')) {
|
||||
$container = $this->containers->where('container.Names', $this->container)->first();
|
||||
$container_name = data_get($container, 'container.Names');
|
||||
if (is_null($container)) {
|
||||
throw new \RuntimeException('Container not found.');
|
||||
}
|
||||
$server = data_get($container, 'server');
|
||||
} else {
|
||||
$container_name = $this->container;
|
||||
$server = $this->servers->first();
|
||||
}
|
||||
if ($server->isForceDisabled()) {
|
||||
throw new \RuntimeException('Server is disabled.');
|
||||
}
|
||||
$cmd = 'sh -c "if [ -f ~/.profile ]; then . ~/.profile; fi; ' . str_replace('"', '\"', $this->command) . '"';
|
||||
if (!empty($this->workDir)) {
|
||||
$exec = "docker exec -w {$this->workDir} {$this->container} {$cmd}";
|
||||
$exec = "docker exec -w {$this->workDir} {$container_name} {$cmd}";
|
||||
} 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);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
|
||||
@@ -23,6 +23,7 @@ class GetLogs extends Component
|
||||
public ServiceApplication|ServiceDatabase|null $servicesubtype = null;
|
||||
public Server $server;
|
||||
public ?string $container = null;
|
||||
public ?string $pull_request = null;
|
||||
public ?bool $streamLogs = false;
|
||||
public ?bool $showTimeStamps = true;
|
||||
public int $numberOfLines = 100;
|
||||
@@ -70,7 +71,14 @@ class GetLogs extends Component
|
||||
}
|
||||
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->showTimeStamps) {
|
||||
if ($this->server->isSwarm()) {
|
||||
|
||||
@@ -10,43 +10,56 @@ use App\Models\StandaloneMongodb;
|
||||
use App\Models\StandaloneMysql;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\StandaloneRedis;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Component;
|
||||
|
||||
class Logs extends Component
|
||||
{
|
||||
public ?string $type = null;
|
||||
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $resource;
|
||||
public Server $server;
|
||||
public Collection $servers;
|
||||
public Collection $containers;
|
||||
public $container = [];
|
||||
public $containers;
|
||||
public $parameters;
|
||||
public $query;
|
||||
public $status;
|
||||
public $serviceSubType;
|
||||
|
||||
public function mount()
|
||||
public function loadContainers($server_id)
|
||||
{
|
||||
$this->containers = 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;
|
||||
$this->server = $this->resource->destination->server;
|
||||
if ($this->server->isSwarm()) {
|
||||
try {
|
||||
$server = $this->servers->firstWhere('id', $server_id);
|
||||
if ($server->isSwarm()) {
|
||||
$containers = collect([
|
||||
[
|
||||
'Names' => $this->resource->uuid . '_' . $this->resource->uuid,
|
||||
]
|
||||
]);
|
||||
} else {
|
||||
$containers = getCurrentApplicationContainerStatus($this->server, $this->resource->id, 0);
|
||||
$containers = getCurrentApplicationContainerStatus($server, $this->resource->id, includePullrequests: true);
|
||||
}
|
||||
if ($containers->count() > 0) {
|
||||
$containers->each(function ($container) {
|
||||
$this->containers->push(str_replace('/', '', $container['Names']));
|
||||
});
|
||||
$server->containers = $containers;
|
||||
} catch (\Exception $e) {
|
||||
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')) {
|
||||
$this->type = 'database';
|
||||
@@ -68,26 +81,24 @@ class Logs extends Component
|
||||
}
|
||||
$this->resource = $resource;
|
||||
$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;
|
||||
// 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')) {
|
||||
$this->type = 'service';
|
||||
$this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail();
|
||||
$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) {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Livewire\Server;
|
||||
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Team;
|
||||
use Livewire\Component;
|
||||
|
||||
class Create extends Component
|
||||
@@ -16,11 +17,7 @@ class Create extends Component
|
||||
$this->limit_reached = false;
|
||||
return;
|
||||
}
|
||||
$team = currentTeam();
|
||||
$servers = $team->servers->count();
|
||||
['serverLimit' => $serverLimit] = $team->limits;
|
||||
|
||||
$this->limit_reached = $servers >= $serverLimit;
|
||||
$this->limit_reached = Team::serverLimitReached();
|
||||
}
|
||||
public function render()
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Livewire\Server\New;
|
||||
use App\Enums\ProxyStatus;
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use Livewire\Component;
|
||||
|
||||
class ByIp extends Component
|
||||
@@ -76,6 +77,9 @@ class ByIp extends Component
|
||||
if (is_null($this->private_key_id)) {
|
||||
return $this->dispatch('error', 'You must select a private key');
|
||||
}
|
||||
if (Team::serverLimitReached()) {
|
||||
return $this->dispatch('error', 'You have reached the server limit for your subscription.');
|
||||
}
|
||||
$payload = [
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
|
||||
@@ -55,7 +55,6 @@ class Resources extends Component
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
$this->loadUnmanagedContainers();
|
||||
}
|
||||
public function render()
|
||||
{
|
||||
|
||||
@@ -2,11 +2,18 @@
|
||||
|
||||
namespace App\Livewire\Subscription;
|
||||
|
||||
use App\Models\Team;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Livewire\Component;
|
||||
|
||||
class Actions extends Component
|
||||
{
|
||||
public $server_limits = 0;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->server_limits = Team::serverLimit();
|
||||
}
|
||||
public function cancel()
|
||||
{
|
||||
try {
|
||||
@@ -69,7 +76,8 @@ class Actions extends Component
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
public function stripeCustomerPortal() {
|
||||
public function stripeCustomerPortal()
|
||||
{
|
||||
$session = getStripeCustomerPortalSession(currentTeam());
|
||||
redirect($session->url);
|
||||
}
|
||||
|
||||
@@ -10,14 +10,19 @@ class Index extends Component
|
||||
{
|
||||
public InstanceSettings $settings;
|
||||
public bool $alreadySubscribed = false;
|
||||
public function mount() {
|
||||
public function mount()
|
||||
{
|
||||
if (!isCloud()) {
|
||||
return redirect(RouteServiceProvider::HOME);
|
||||
}
|
||||
if (data_get(currentTeam(), 'subscription')) {
|
||||
return redirect()->route('subscription.show');
|
||||
}
|
||||
$this->settings = InstanceSettings::get();
|
||||
$this->alreadySubscribed = currentTeam()->subscription()->exists();
|
||||
}
|
||||
public function stripeCustomerPortal() {
|
||||
public function stripeCustomerPortal()
|
||||
{
|
||||
$session = getStripeCustomerPortalSession(currentTeam());
|
||||
if (is_null($session)) {
|
||||
return;
|
||||
|
||||
@@ -9,8 +9,9 @@ use Stripe\Checkout\Session;
|
||||
class PricingPlans extends Component
|
||||
{
|
||||
public bool $isTrial = false;
|
||||
public function mount() {
|
||||
$this->isTrial = !data_get(currentTeam(),'subscription.stripe_trial_already_ended');
|
||||
public function mount()
|
||||
{
|
||||
$this->isTrial = !data_get(currentTeam(), 'subscription.stripe_trial_already_ended');
|
||||
if (config('constants.limits.trial_period') == 0) {
|
||||
$this->isTrial = false;
|
||||
}
|
||||
@@ -26,15 +27,15 @@ class PricingPlans extends Component
|
||||
case 'basic-yearly':
|
||||
$priceId = config('subscription.stripe_price_id_basic_yearly');
|
||||
break;
|
||||
case 'ultimate-monthly':
|
||||
$priceId = config('subscription.stripe_price_id_ultimate_monthly');
|
||||
break;
|
||||
case 'pro-monthly':
|
||||
$priceId = config('subscription.stripe_price_id_pro_monthly');
|
||||
break;
|
||||
case 'pro-yearly':
|
||||
$priceId = config('subscription.stripe_price_id_pro_yearly');
|
||||
break;
|
||||
case 'ultimate-monthly':
|
||||
$priceId = config('subscription.stripe_price_id_ultimate_monthly');
|
||||
break;
|
||||
case 'ultimate-yearly':
|
||||
$priceId = config('subscription.stripe_price_id_ultimate_yearly');
|
||||
break;
|
||||
@@ -64,18 +65,25 @@ class PricingPlans extends Component
|
||||
'success_url' => route('dashboard', ['success' => true]),
|
||||
'cancel_url' => route('subscription.index', ['cancelled' => true]),
|
||||
];
|
||||
|
||||
if (!data_get($team,'subscription.stripe_trial_already_ended')) {
|
||||
if (config('constants.limits.trial_period') > 0) {
|
||||
$payload['subscription_data'] = [
|
||||
'trial_period_days' => config('constants.limits.trial_period'),
|
||||
'trial_settings' => [
|
||||
'end_behavior' => [
|
||||
'missing_payment_method' => 'cancel',
|
||||
]
|
||||
],
|
||||
if (str($type)->contains('ultimate')) {
|
||||
$payload['line_items'][0]['adjustable_quantity'] = [
|
||||
'enabled' => true,
|
||||
'minimum' => 10,
|
||||
];
|
||||
$payload['line_items'][0]['quantity'] = 10;
|
||||
}
|
||||
|
||||
if (!data_get($team, 'subscription.stripe_trial_already_ended')) {
|
||||
if (config('constants.limits.trial_period') > 0) {
|
||||
$payload['subscription_data'] = [
|
||||
'trial_period_days' => config('constants.limits.trial_period'),
|
||||
'trial_settings' => [
|
||||
'end_behavior' => [
|
||||
'missing_payment_method' => 'cancel',
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
$payload['payment_method_collection'] = 'if_required';
|
||||
}
|
||||
$customer = currentTeam()->subscription?->stripe_customer_id ?? null;
|
||||
|
||||
22
app/Livewire/Subscription/Show.php
Normal file
22
app/Livewire/Subscription/Show.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Subscription;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
class Show extends Component
|
||||
{
|
||||
public function mount()
|
||||
{
|
||||
if (!isCloud()) {
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
if (!data_get(currentTeam(), 'subscription')) {
|
||||
return redirect()->route('subscription.index');
|
||||
}
|
||||
}
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.subscription.show');
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,11 @@ class Member extends Component
|
||||
$this->dispatch('reloadWindow');
|
||||
}
|
||||
|
||||
public function makeOwner()
|
||||
{
|
||||
$this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'owner']);
|
||||
$this->dispatch('reloadWindow');
|
||||
}
|
||||
public function makeReadonly()
|
||||
{
|
||||
$this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'member']);
|
||||
@@ -26,7 +31,7 @@ class Member extends Component
|
||||
{
|
||||
$this->member->teams()->detach(currentTeam());
|
||||
Cache::forget("team:{$this->member->id}");
|
||||
Cache::remember('team:' . $this->member->id, 3600, function() {
|
||||
Cache::remember('team:' . $this->member->id, 3600, function () {
|
||||
return $this->member->teams()->first();
|
||||
});
|
||||
$this->dispatch('reloadWindow');
|
||||
|
||||
@@ -65,6 +65,13 @@ class Application extends BaseModel
|
||||
return $this->belongsToMany(StandaloneDocker::class, 'additional_destinations')
|
||||
->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
|
||||
{
|
||||
if (data_get($this, 'source')) {
|
||||
@@ -395,7 +402,10 @@ class Application extends BaseModel
|
||||
}
|
||||
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)
|
||||
{
|
||||
$deployments = ApplicationDeploymentQueue::where('application_id', $this->id)->orderBy('created_at', 'desc');
|
||||
|
||||
@@ -30,4 +30,8 @@ class ScheduledDatabaseBackup extends BaseModel
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\Notifications\Server\Unreachable;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Spatie\SchemalessAttributes\Casts\SchemalessAttributes;
|
||||
use Spatie\SchemalessAttributes\SchemalessAttributesTrait;
|
||||
use Illuminate\Support\Str;
|
||||
@@ -69,7 +70,7 @@ class Server extends BaseModel
|
||||
|
||||
static public function isUsable()
|
||||
{
|
||||
return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_swarm_worker', false)->whereRelation('settings', 'is_build_server', false);
|
||||
return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_swarm_worker', false)->whereRelation('settings', 'is_build_server', false)->whereRelation('settings', 'force_disabled', false);
|
||||
}
|
||||
|
||||
static public function destinationsByServer(string $server_id)
|
||||
@@ -146,11 +147,34 @@ class Server extends BaseModel
|
||||
public function skipServer()
|
||||
{
|
||||
if ($this->ip === '1.2.3.4') {
|
||||
ray('skipping 1.2.3.4');
|
||||
// ray('skipping 1.2.3.4');
|
||||
return true;
|
||||
}
|
||||
if ($this->settings->force_disabled === true) {
|
||||
// ray('force_disabled');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public function isForceDisabled()
|
||||
{
|
||||
return $this->settings->force_disabled;
|
||||
}
|
||||
public function forceEnableServer()
|
||||
{
|
||||
$this->settings->update([
|
||||
'force_disabled' => false,
|
||||
]);
|
||||
}
|
||||
public function forceDisableServer()
|
||||
{
|
||||
$this->settings->update([
|
||||
'force_disabled' => true,
|
||||
]);
|
||||
$sshKeyFileLocation = "id.root@{$this->uuid}";
|
||||
Storage::disk('ssh-keys')->delete($sshKeyFileLocation);
|
||||
Storage::disk('ssh-mux')->delete($this->muxFilename());
|
||||
}
|
||||
public function isServerReady(int $tries = 3)
|
||||
{
|
||||
if ($this->skipServer()) {
|
||||
@@ -239,17 +263,21 @@ class Server extends BaseModel
|
||||
}
|
||||
public function loadUnmanagedContainers()
|
||||
{
|
||||
$containers = instant_remote_process(["docker ps -a --format '{{json .}}' "], $this);
|
||||
$containers = format_docker_command_output_to_json($containers);
|
||||
$containers = $containers->map(function ($container) {
|
||||
$labels = data_get($container, 'Labels');
|
||||
if (!str($labels)->contains("coolify.managed")) {
|
||||
return $container;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
$containers = $containers->filter();
|
||||
return collect($containers);
|
||||
if ($this->isFunctional()) {
|
||||
$containers = instant_remote_process(["docker ps -a --format '{{json .}}' "], $this);
|
||||
$containers = format_docker_command_output_to_json($containers);
|
||||
$containers = $containers->map(function ($container) {
|
||||
$labels = data_get($container, 'Labels');
|
||||
if (!str($labels)->contains("coolify.managed")) {
|
||||
return $container;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
$containers = $containers->filter();
|
||||
return collect($containers);
|
||||
} else {
|
||||
return collect([]);
|
||||
}
|
||||
}
|
||||
public function hasDefinedResources()
|
||||
{
|
||||
@@ -374,7 +402,7 @@ class Server extends BaseModel
|
||||
}
|
||||
public function isFunctional()
|
||||
{
|
||||
return $this->settings->is_reachable && $this->settings->is_usable;
|
||||
return $this->settings->is_reachable && $this->settings->is_usable && !$this->settings->force_disabled;
|
||||
}
|
||||
public function isLogDrainEnabled()
|
||||
{
|
||||
|
||||
@@ -6,7 +6,6 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class Service extends BaseModel
|
||||
{
|
||||
@@ -28,47 +27,73 @@ class Service extends BaseModel
|
||||
{
|
||||
return $this->morphToMany(Tag::class, 'taggable');
|
||||
}
|
||||
public function status() {
|
||||
$foundRunning = false;
|
||||
$isDegraded = false;
|
||||
$foundRestaring = false;
|
||||
public function status()
|
||||
{
|
||||
$applications = $this->applications;
|
||||
$databases = $this->databases;
|
||||
|
||||
$complexStatus = null;
|
||||
$complexHealth = null;
|
||||
|
||||
foreach ($applications as $application) {
|
||||
if ($application->exclude_from_status) {
|
||||
continue;
|
||||
}
|
||||
if (Str::of($application->status)->startsWith('running')) {
|
||||
$foundRunning = true;
|
||||
} else if (Str::of($application->status)->startsWith('restarting')) {
|
||||
$foundRestaring = true;
|
||||
$status = str($application->status)->before('(')->trim();
|
||||
$health = str($application->status)->between('(', ')')->trim();
|
||||
if ($complexStatus === 'degraded') {
|
||||
continue;
|
||||
}
|
||||
if ($status->startsWith('running')) {
|
||||
if ($complexStatus === 'exited') {
|
||||
$complexStatus = 'degraded';
|
||||
} else {
|
||||
$complexStatus = 'running';
|
||||
}
|
||||
} else if ($status->startsWith('restarting')) {
|
||||
$complexStatus = 'degraded';
|
||||
} else if ($status->startsWith('exited')) {
|
||||
$complexStatus = 'exited';
|
||||
}
|
||||
if ($health->value() === 'healthy') {
|
||||
if ($complexHealth === 'unhealthy') {
|
||||
continue;
|
||||
}
|
||||
$complexHealth = 'healthy';
|
||||
} else {
|
||||
$isDegraded = true;
|
||||
$complexHealth = 'unhealthy';
|
||||
}
|
||||
}
|
||||
foreach ($databases as $database) {
|
||||
if ($database->exclude_from_status) {
|
||||
continue;
|
||||
}
|
||||
if (Str::of($database->status)->startsWith('running')) {
|
||||
$foundRunning = true;
|
||||
} else if (Str::of($database->status)->startsWith('restarting')) {
|
||||
$foundRestaring = true;
|
||||
$status = str($database->status)->before('(')->trim();
|
||||
$health = str($database->status)->between('(', ')')->trim();
|
||||
if ($complexStatus === 'degraded') {
|
||||
continue;
|
||||
}
|
||||
if ($status->startsWith('running')) {
|
||||
if ($complexStatus === 'exited') {
|
||||
$complexStatus = 'degraded';
|
||||
} else {
|
||||
$complexStatus = 'running';
|
||||
}
|
||||
} else if ($status->startsWith('restarting')) {
|
||||
$complexStatus = 'degraded';
|
||||
} else if ($status->startsWith('exited')) {
|
||||
$complexStatus = 'exited';
|
||||
}
|
||||
if ($health->value() === 'healthy') {
|
||||
if ($complexHealth === 'unhealthy') {
|
||||
continue;
|
||||
}
|
||||
$complexHealth = 'healthy';
|
||||
} else {
|
||||
$isDegraded = true;
|
||||
$complexHealth = 'unhealthy';
|
||||
}
|
||||
}
|
||||
if ($foundRestaring) {
|
||||
return 'degraded';
|
||||
}
|
||||
if ($foundRunning && !$isDegraded) {
|
||||
return 'running';
|
||||
} else if ($foundRunning && $isDegraded) {
|
||||
return 'degraded';
|
||||
} else if (!$foundRunning && !$isDegraded) {
|
||||
return 'exited';
|
||||
}
|
||||
return 'exited';
|
||||
return "{$complexStatus}:{$complexHealth}";
|
||||
}
|
||||
public function extraFields()
|
||||
{
|
||||
@@ -77,6 +102,30 @@ class Service extends BaseModel
|
||||
foreach ($applications as $application) {
|
||||
$image = str($application->image)->before(':')->value();
|
||||
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'):
|
||||
$data = collect([]);
|
||||
$console_url = $this->environment_variables()->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first();
|
||||
@@ -414,7 +463,7 @@ class Service extends BaseModel
|
||||
public function documentation()
|
||||
{
|
||||
$services = getServiceTemplates();
|
||||
$service = data_get($services, Str::of($this->name)->beforeLast('-')->value, []);
|
||||
$service = data_get($services, str($this->name)->beforeLast('-')->value, []);
|
||||
return data_get($service, 'documentation', config('constants.docs.base_url'));
|
||||
}
|
||||
public function applications()
|
||||
|
||||
@@ -30,8 +30,7 @@ class Subscription extends Model
|
||||
if (in_array($subscription, $ultimate)) {
|
||||
return 'ultimate';
|
||||
}
|
||||
}
|
||||
if (isStripe()) {
|
||||
} else if (isStripe()) {
|
||||
if (!$this->stripe_plan_id) {
|
||||
return 'zero';
|
||||
}
|
||||
@@ -55,7 +54,7 @@ class Subscription extends Model
|
||||
};
|
||||
})->first();
|
||||
if ($stripePlanId) {
|
||||
return Str::of($stripePlanId)->after('stripe_price_id_')->before('_')->lower();
|
||||
return str($stripePlanId)->after('stripe_price_id_')->before('_')->lower();
|
||||
}
|
||||
}
|
||||
return 'zero';
|
||||
|
||||
@@ -48,7 +48,25 @@ class Team extends Model implements SendsDiscord, SendsEmail
|
||||
}
|
||||
return explode(',', $recipients);
|
||||
}
|
||||
|
||||
static public function serverLimitReached() {
|
||||
$serverLimit = Team::serverLimit();
|
||||
$team = currentTeam();
|
||||
$servers = $team->servers->count();
|
||||
return $servers >= $serverLimit;
|
||||
}
|
||||
public function serverOverflow() {
|
||||
if ($this->serverLimit() < $this->servers->count()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
static public function serverLimit()
|
||||
{
|
||||
if (currentTeam()->id === 0 && isDev()) {
|
||||
return 9999999;
|
||||
}
|
||||
return Team::find(currentTeam()->id)->limits['serverLimit'];
|
||||
}
|
||||
public function limits(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
@@ -63,14 +81,19 @@ class Team extends Model implements SendsDiscord, SendsEmail
|
||||
$subscription = $subscription->type();
|
||||
}
|
||||
}
|
||||
$serverLimit = config('constants.limits.server')[strtolower($subscription)];
|
||||
if ($this->custom_server_limit) {
|
||||
$serverLimit = $this->custom_server_limit;
|
||||
} else {
|
||||
$serverLimit = config('constants.limits.server')[strtolower($subscription)];
|
||||
}
|
||||
$sharedEmailEnabled = config('constants.limits.email')[strtolower($subscription)];
|
||||
return ['serverLimit' => $serverLimit, 'sharedEmailEnabled' => $sharedEmailEnabled];
|
||||
}
|
||||
|
||||
);
|
||||
}
|
||||
public function environment_variables() {
|
||||
public function environment_variables()
|
||||
{
|
||||
return $this->hasMany(SharedEnvironmentVariable::class)->whereNull('project_id')->whereNull('environment_id');
|
||||
}
|
||||
public function members()
|
||||
@@ -130,7 +153,8 @@ class Team extends Model implements SendsDiscord, SendsEmail
|
||||
{
|
||||
return $this->hasMany(S3Storage::class)->where('is_usable', true);
|
||||
}
|
||||
public function trialEnded() {
|
||||
public function trialEnded()
|
||||
{
|
||||
foreach ($this->servers as $server) {
|
||||
$server->settings()->update([
|
||||
'is_usable' => false,
|
||||
@@ -138,7 +162,8 @@ class Team extends Model implements SendsDiscord, SendsEmail
|
||||
]);
|
||||
}
|
||||
}
|
||||
public function trialEndedButSubscribed() {
|
||||
public function trialEndedButSubscribed()
|
||||
{
|
||||
foreach ($this->servers as $server) {
|
||||
$server->settings()->update([
|
||||
'is_usable' => true,
|
||||
|
||||
@@ -67,7 +67,7 @@ class User extends Authenticatable implements SendsEmail
|
||||
'team_id' => session('currentTeam')->id
|
||||
]);
|
||||
|
||||
return new NewAccessToken($token, $token->getKey().'|'.$plainTextToken);
|
||||
return new NewAccessToken($token, $token->getKey() . '|' . $plainTextToken);
|
||||
}
|
||||
public function teams()
|
||||
{
|
||||
@@ -103,9 +103,13 @@ class User extends Authenticatable implements SendsEmail
|
||||
|
||||
public function isAdmin()
|
||||
{
|
||||
return data_get($this->pivot, 'role') === 'admin' || data_get($this->pivot, 'role') === 'owner';
|
||||
return $this->role() === 'admin' || $this->role() === 'owner';
|
||||
}
|
||||
|
||||
public function isOwner()
|
||||
{
|
||||
return $this->role() === 'owner';
|
||||
}
|
||||
public function isAdminFromSession()
|
||||
{
|
||||
if (auth()->user()->id === 0) {
|
||||
@@ -155,6 +159,9 @@ class User extends Authenticatable implements SendsEmail
|
||||
|
||||
public function role()
|
||||
{
|
||||
return session('currentTeam')->pivot->role;
|
||||
if (data_get($this, 'pivot')) {
|
||||
return $this->pivot->role;
|
||||
}
|
||||
return auth()->user()->teams->where('id', currentTeam()->id)->first()->pivot->role;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,13 @@ class DeploymentSuccess extends Notification implements ShouldQueue
|
||||
|
||||
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
|
||||
@@ -69,7 +75,7 @@ class DeploymentSuccess extends Notification implements ShouldQueue
|
||||
public function toDiscord(): string
|
||||
{
|
||||
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) {
|
||||
|
||||
50
app/Notifications/Database/DailyBackup.php
Normal file
50
app/Notifications/Database/DailyBackup.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
63
app/Notifications/Server/ForceDisabled.php
Normal file
63
app/Notifications/Server/ForceDisabled.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications\Server;
|
||||
|
||||
use App\Models\Server;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use App\Notifications\Channels\DiscordChannel;
|
||||
use App\Notifications\Channels\EmailChannel;
|
||||
use App\Notifications\Channels\TelegramChannel;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class ForceDisabled extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public $tries = 1;
|
||||
public function __construct(public Server $server)
|
||||
{
|
||||
}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
$channels = [];
|
||||
$isEmailEnabled = isEmailEnabled($notifiable);
|
||||
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
|
||||
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
|
||||
|
||||
if ($isDiscordEnabled) {
|
||||
$channels[] = DiscordChannel::class;
|
||||
}
|
||||
if ($isEmailEnabled) {
|
||||
$channels[] = EmailChannel::class;
|
||||
}
|
||||
if ($isTelegramEnabled) {
|
||||
$channels[] = TelegramChannel::class;
|
||||
}
|
||||
return $channels;
|
||||
}
|
||||
|
||||
public function toMail(): MailMessage
|
||||
{
|
||||
$mail = new MailMessage();
|
||||
$mail->subject("Coolify: Server ({$this->server->name}) disabled because it is not paid!");
|
||||
$mail->view('emails.server-force-disabled', [
|
||||
'name' => $this->server->name,
|
||||
]);
|
||||
return $mail;
|
||||
}
|
||||
|
||||
public function toDiscord(): string
|
||||
{
|
||||
$message = "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subsciprtions).";
|
||||
return $message;
|
||||
}
|
||||
public function toTelegram(): array
|
||||
{
|
||||
return [
|
||||
"message" => "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subsciprtions)."
|
||||
];
|
||||
}
|
||||
}
|
||||
63
app/Notifications/Server/ForceEnabled.php
Normal file
63
app/Notifications/Server/ForceEnabled.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications\Server;
|
||||
|
||||
use App\Models\Server;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use App\Notifications\Channels\DiscordChannel;
|
||||
use App\Notifications\Channels\EmailChannel;
|
||||
use App\Notifications\Channels\TelegramChannel;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class ForceEnabled extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public $tries = 1;
|
||||
public function __construct(public Server $server)
|
||||
{
|
||||
}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
$channels = [];
|
||||
$isEmailEnabled = isEmailEnabled($notifiable);
|
||||
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
|
||||
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
|
||||
|
||||
if ($isDiscordEnabled) {
|
||||
$channels[] = DiscordChannel::class;
|
||||
}
|
||||
if ($isEmailEnabled) {
|
||||
$channels[] = EmailChannel::class;
|
||||
}
|
||||
if ($isTelegramEnabled) {
|
||||
$channels[] = TelegramChannel::class;
|
||||
}
|
||||
return $channels;
|
||||
}
|
||||
|
||||
public function toMail(): MailMessage
|
||||
{
|
||||
$mail = new MailMessage();
|
||||
$mail->subject("Coolify: Server ({$this->server->name}) enabled again!");
|
||||
$mail->view('emails.server-force-enabled', [
|
||||
'name' => $this->server->name,
|
||||
]);
|
||||
return $mail;
|
||||
}
|
||||
|
||||
public function toDiscord(): string
|
||||
{
|
||||
$message = "Coolify: Server ({$this->server->name}) enabled again!";
|
||||
return $message;
|
||||
}
|
||||
public function toTelegram(): array
|
||||
{
|
||||
return [
|
||||
"message" => "Coolify: Server ({$this->server->name}) enabled again!"
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,21 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
|
||||
use App\Listeners\MaintenanceModeDisabledNotification;
|
||||
use App\Listeners\MaintenanceModeEnabledNotification;
|
||||
use Illuminate\Foundation\Events\MaintenanceModeDisabled;
|
||||
use Illuminate\Foundation\Events\MaintenanceModeEnabled;
|
||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
||||
|
||||
class EventServiceProvider extends ServiceProvider
|
||||
{
|
||||
protected $listen = [
|
||||
MaintenanceModeEnabled::class => [
|
||||
MaintenanceModeEnabledNotification::class,
|
||||
],
|
||||
MaintenanceModeDisabled::class => [
|
||||
MaintenanceModeDisabledNotification::class,
|
||||
],
|
||||
// Registered::class => [
|
||||
// SendEmailVerificationNotification::class,
|
||||
// ],
|
||||
|
||||
@@ -11,12 +11,11 @@ class Index extends Component
|
||||
/**
|
||||
* Create a new component instance.
|
||||
*/
|
||||
public $status = "exited:unhealthy";
|
||||
|
||||
public function __construct(
|
||||
public $resource = null,
|
||||
public bool $showRefreshButton = true,
|
||||
) {
|
||||
$this->status = $resource->status;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,18 +8,21 @@ use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
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([]);
|
||||
if (!$server->isSwarm()) {
|
||||
$containers = instant_remote_process(["docker ps -a --filter='label=coolify.applicationId={$id}' --format '{{json .}}' "], $server);
|
||||
$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');
|
||||
if (!str($labels)->contains("coolify.pullRequestId=")) {
|
||||
data_set($container, 'Labels', $labels . ",coolify.pullRequestId={$pullRequestId}");
|
||||
return $container;
|
||||
}
|
||||
if ($includePullrequests) {
|
||||
return $container;
|
||||
}
|
||||
if (str($labels)->contains("coolify.pullRequestId=$pullRequestId")) {
|
||||
return $container;
|
||||
}
|
||||
@@ -423,7 +426,7 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null
|
||||
'--security-opt',
|
||||
'--sysctl',
|
||||
'--ulimit',
|
||||
'--device'
|
||||
'--device',
|
||||
]);
|
||||
$mapping = collect([
|
||||
'--cap-add' => 'cap_add',
|
||||
@@ -435,6 +438,7 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null
|
||||
'--init' => 'init',
|
||||
'--ulimit' => 'ulimits',
|
||||
'--privileged' => 'privileged',
|
||||
'--ip' => 'ip',
|
||||
]);
|
||||
foreach ($matches as $match) {
|
||||
$option = $match[1];
|
||||
|
||||
@@ -110,6 +110,9 @@ function instant_scp(string $source, string $dest, Server $server, $throwError =
|
||||
}
|
||||
function generateSshCommand(Server $server, string $command)
|
||||
{
|
||||
if ($server->settings->force_disabled) {
|
||||
throw new \RuntimeException('Server is disabled.');
|
||||
}
|
||||
$user = $server->user;
|
||||
$port = $server->port;
|
||||
$privateKeyLocation = savePrivateKeyToFs($server);
|
||||
|
||||
@@ -33,6 +33,11 @@ use Illuminate\Support\Facades\Request;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Str;
|
||||
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 Visus\Cuid2\Cuid2;
|
||||
use phpseclib3\Crypt\RSA;
|
||||
@@ -625,7 +630,6 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
}
|
||||
}
|
||||
$definedNetwork = collect([$resource->uuid]);
|
||||
|
||||
$services = collect($services)->map(function ($service, $serviceName) use ($topLevelVolumes, $topLevelNetworks, $definedNetwork, $isNew, $generatedServiceFQDNS, $resource) {
|
||||
$serviceVolumes = collect(data_get($service, 'volumes', []));
|
||||
$servicePorts = collect(data_get($service, 'ports', []));
|
||||
@@ -927,6 +931,13 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
$savedService->fqdn = $fqdn;
|
||||
$savedService->save();
|
||||
}
|
||||
EnvironmentVariable::create([
|
||||
'key' => $key,
|
||||
'value' => $fqdn,
|
||||
'is_build_time' => false,
|
||||
'service_id' => $resource->id,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
}
|
||||
// data_forget($service, "environment.$variableName");
|
||||
// $yaml = data_forget($yaml, "services.$serviceName.environment.$variableName");
|
||||
@@ -978,7 +989,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$generatedValue = generateEnvValue($command);
|
||||
$generatedValue = generateEnvValue($command, $resource);
|
||||
if (!$foundEnv) {
|
||||
EnvironmentVariable::create([
|
||||
'key' => $key,
|
||||
@@ -1394,7 +1405,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
$generatedValue = generateEnvValue($command);
|
||||
$generatedValue = generateEnvValue($command, $service);
|
||||
if (!$foundEnv) {
|
||||
EnvironmentVariable::create([
|
||||
'key' => $key,
|
||||
@@ -1570,7 +1581,7 @@ function parseEnvVariable(Str|string $value)
|
||||
'port' => $port,
|
||||
];
|
||||
}
|
||||
function generateEnvValue(string $command)
|
||||
function generateEnvValue(string $command, Service $service)
|
||||
{
|
||||
switch ($command) {
|
||||
case 'PASSWORD':
|
||||
@@ -1591,6 +1602,46 @@ function generateEnvValue(string $command)
|
||||
case 'USER':
|
||||
$generatedValue = Str::random(16);
|
||||
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:
|
||||
$generatedValue = Str::random(16);
|
||||
break;
|
||||
|
||||
@@ -109,7 +109,7 @@ function isPaddle()
|
||||
function getStripeCustomerPortalSession(Team $team)
|
||||
{
|
||||
Stripe::setApiKey(config('subscription.stripe_api_key'));
|
||||
$return_url = route('team.index');
|
||||
$return_url = route('subscription.show');
|
||||
$stripe_customer_id = data_get($team,'subscription.stripe_customer_id');
|
||||
if (!$stripe_customer_id) {
|
||||
return null;
|
||||
@@ -123,7 +123,7 @@ function getStripeCustomerPortalSession(Team $team)
|
||||
function allowedPathsForUnsubscribedAccounts()
|
||||
{
|
||||
return [
|
||||
'subscription',
|
||||
'subscription/new',
|
||||
'login',
|
||||
'logout',
|
||||
'waitlist',
|
||||
|
||||
@@ -35,6 +35,13 @@ return [
|
||||
'throw' => false,
|
||||
],
|
||||
|
||||
'webhooks-during-maintenance' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/webhooks-during-maintenance'),
|
||||
'visibility' => 'private',
|
||||
'throw' => false,
|
||||
],
|
||||
|
||||
'public' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/public'),
|
||||
|
||||
@@ -7,7 +7,7 @@ return [
|
||||
|
||||
// The release version of your application
|
||||
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
|
||||
'release' => '4.0.0-beta.222',
|
||||
'release' => '4.0.0-beta.231',
|
||||
// When left empty or `null` the Laravel environment will be used
|
||||
'environment' => config('app.env'),
|
||||
|
||||
|
||||
@@ -13,6 +13,12 @@ return [
|
||||
'stripe_price_id_ultimate_yearly' => env('STRIPE_PRICE_ID_ULTIMATE_YEARLY', null),
|
||||
'stripe_excluded_plans' => env('STRIPE_EXCLUDED_PLANS', null),
|
||||
|
||||
'stripe_price_id_basic_monthly_old' => env('STRIPE_PRICE_ID_BASIC_MONTHLY_OLD', null),
|
||||
'stripe_price_id_basic_yearly_old' => env('STRIPE_PRICE_ID_BASIC_YEARLY_OLD', null),
|
||||
'stripe_price_id_pro_monthly_old' => env('STRIPE_PRICE_ID_PRO_MONTHLY_OLD', null),
|
||||
'stripe_price_id_pro_yearly_old' => env('STRIPE_PRICE_ID_PRO_YEARLY_OLD', null),
|
||||
'stripe_price_id_ultimate_monthly_old' => env('STRIPE_PRICE_ID_ULTIMATE_MONTHLY_OLD', null),
|
||||
'stripe_price_id_ultimate_yearly_old' => env('STRIPE_PRICE_ID_ULTIMATE_YEARLY_OLD', null),
|
||||
|
||||
// Paddle
|
||||
'paddle_vendor_id' => env('PADDLE_VENDOR_ID', null),
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<?php
|
||||
|
||||
return '4.0.0-beta.222';
|
||||
return '4.0.0-beta.231';
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('teams', function (Blueprint $table) {
|
||||
$table->integer('custom_server_limit')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('teams', function (Blueprint $table) {
|
||||
$table->dropColumn('custom_server_limit');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('server_settings', function (Blueprint $table) {
|
||||
$table->boolean('force_disabled')->default(false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('server_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('force_disabled');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -18,7 +18,6 @@ class DatabaseSeeder extends Seeder
|
||||
ProjectSeeder::class,
|
||||
ProjectSettingSeeder::class,
|
||||
EnvironmentSeeder::class,
|
||||
TeamEnvironmentVariableSeeder::class,
|
||||
StandaloneDockerSeeder::class,
|
||||
SwarmDockerSeeder::class,
|
||||
KubernetesSeeder::class,
|
||||
|
||||
@@ -12,6 +12,7 @@ services:
|
||||
- /data/coolify/databases:/var/www/html/storage/app/databases
|
||||
- /data/coolify/services:/var/www/html/storage/app/services
|
||||
- /data/coolify/backups:/var/www/html/storage/app/backups
|
||||
- /data/coolify/webhooks-during-maintenance:/var/www/html/storage/app/webhooks-during-maintenance
|
||||
environment:
|
||||
- APP_ID
|
||||
- APP_ENV=production
|
||||
@@ -57,6 +58,12 @@ services:
|
||||
- STRIPE_PRICE_ID_PRO_YEARLY
|
||||
- STRIPE_PRICE_ID_ULTIMATE_MONTHLY
|
||||
- STRIPE_PRICE_ID_ULTIMATE_YEARLY
|
||||
- STRIPE_PRICE_ID_BASIC_MONTHLY_OLD
|
||||
- STRIPE_PRICE_ID_BASIC_YEARLY_OLD
|
||||
- STRIPE_PRICE_ID_PRO_MONTHLY_OLD
|
||||
- STRIPE_PRICE_ID_PRO_YEARLY_OLD
|
||||
- STRIPE_PRICE_ID_ULTIMATE_MONTHLY_OLD
|
||||
- STRIPE_PRICE_ID_ULTIMATE_YEARLY_OLD
|
||||
- STRIPE_EXCLUDED_PLANS
|
||||
- PADDLE_VENDOR_ID
|
||||
- PADDLE_WEBHOOK_SECRET
|
||||
|
||||
@@ -26,6 +26,7 @@ services:
|
||||
- ./databases:/var/www/html/storage/app/databases
|
||||
- ./services:/var/www/html/storage/app/services
|
||||
- ./backups:/var/www/html/storage/app/backups
|
||||
- ./webhooks-during-maintenance:/var/www/html/storage/app/webhooks-during-maintenance
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
|
||||
21
public/svgs/firefly.svg
Normal file
21
public/svgs/firefly.svg
Normal file
@@ -0,0 +1,21 @@
|
||||
<!--
|
||||
- maskable-icon.svg
|
||||
- Copyright (c) 2022 james@firefly-iii.org
|
||||
-
|
||||
- This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
-
|
||||
- This program is free software: you can redistribute it and/or modify
|
||||
- it under the terms of the GNU Affero General Public License as
|
||||
- published by the Free Software Foundation, either version 3 of the
|
||||
- License, or (at your option) any later version.
|
||||
-
|
||||
- This program is distributed in the hope that it will be useful,
|
||||
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
- GNU Affero General Public License for more details.
|
||||
-
|
||||
- You should have received a copy of the GNU Affero General Public License
|
||||
- along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<svg height="377.95276" width="377.95276" xmlns="http://www.w3.org/2000/svg"><path d="m0 0h377.95276v377.95276h-377.95276z" fill="#cd5029" stroke-width="1.96129"/><g transform="matrix(.77452773 0 0 .77452773 21.636074 21.374655)"><path d="m140.49013 78.646381 2.249 53.017999s-40.103 29.566-45.538 68l-16.001 1.231s-11.539 2.564-11.539 14.103v37.18s3.846 11.538 12.82 11.538l16.487-.319s8 30.5 36.5 50.5v25.5s-2 8.5 15.5 11 40.75 2.25 44.5-1.5 3.75-4.5 3.75-9c0 0 21.25 5 60.25 0v5s3.5 7 29 7 33-3 37.5-12v-25s37.009-36.264 35.75-91.75c-1.083-47.75-15.901-64.299-35.806-82.96-22.67-21.254-69.944-31.165-117.944-25.353.001-.001-24.341-43.937999-67.478-36.187999z" fill="#fff"/><circle cx="135.46912" cy="214.39638" fill="#cd5029" r="9.5"/><path d="m360.08113 190.51238s-18.218-8.742-40.662 3.996c0 0-26.711-8.987-40.99 2.593-14.828 12.025-16.299 26.115-15.525 42.785 0 0 12.837-43.915 45.252-32.571 0 0-22.947 40.43 12.761 47.508 0 0 8.436-.05 15.401-4.256 6.644-4.011 11.842-11.433 9.711-24.814 0 0-4.348-13.336-15.569-21.42 0 0 11.042-7.806 31.988-2.209z" fill="#cd5029"/><path d="m320.19013 213.01938s-16.689 31.461 5.607 29.767c0 0 11.838-5.656 4.887-17.127-7.147-11.796-10.494-12.64-10.494-12.64z" fill="#fff"/></g><path d="m188.97638 175.70052s4.01698 13.60604-3.69586 21.52748c-7.713 7.92145-6.8792 16.6767-3.75227 20.84588 3.12692 4.16917 2.91831 7.29593.41674 9.58905-2.50141 2.29312-4.58608 3.96073-6.04523.20846-1.45916-3.75228-3.12676-3.75228-3.75228-5.62834-.62552-1.87605-1.87622-5.21142-1.87622-5.21142s-3.96072 6.25384-6.46229 10.00611c-2.50157 3.75228-2.50141 9.58922-.83381 12.71598 1.66761 3.12676 1.04226 6.87903-.20845 12.09046-1.2507 5.21143.4169 13.13288 6.25369 16.2598 5.83678 3.12692 12.92459 5.62833 16.05135 8.5468s10.42301 5.62833 19.80362 3.54382c9.3806-2.0845 21.26294-11.67355 23.34744-18.13585 0 0 5.41988-6.04523 4.37763-13.96668s-4.79469-7.71316-6.4623-13.75839c-1.6676-6.04523 3.60854-4.55469-.8338-14.93382 0 0-1.98012-4.94005-9.50352-8.49899-4.83404-2.28661-1.54469-12.63061-10.09149-23.05347s-16.73295-12.14688-16.73295-12.14688z" fill="#ffa284" stroke-width=".162598"/></svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
3
public/svgs/internal-link.svg
Normal file
3
public/svgs/internal-link.svg
Normal 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
15
public/svgs/supabase.svg
Normal 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 |
22
resources/views/components/banner.blade.php
Normal file
22
resources/views/components/banner.blade.php
Normal file
@@ -0,0 +1,22 @@
|
||||
@props(['closable' => true])
|
||||
<div x-data="{
|
||||
bannerVisible: false,
|
||||
bannerVisibleAfter: 100,
|
||||
}" x-show="bannerVisible" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="-translate-y-10" x-transition:enter-end="translate-y-0"
|
||||
x-transition:leave="transition ease-in duration-100" x-transition:leave-start="translate-y-0"
|
||||
x-transition:leave-end="-translate-y-10" x-init="setTimeout(() => { bannerVisible = true }, bannerVisibleAfter);"
|
||||
class="relative z-50 w-full py-2 mx-auto duration-100 ease-out shadow-sm bg-coolgray-100 sm:py-0 sm:h-14" x-cloak>
|
||||
<div class="flex items-center justify-between h-full px-3">
|
||||
{{ $slot }}
|
||||
@if ($closable)
|
||||
<button @click="bannerVisible=false"
|
||||
class="flex items-center flex-shrink-0 translate-x-1 ease-out duration-150 justify-center w-6 h-6 p-1.5 text-neutral-200 rounded-full hover:bg-coolgray-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" class="w-full h-full">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
1
resources/views/components/internal-link.blade.php
Normal file
1
resources/views/components/internal-link.blade.php
Normal file
@@ -0,0 +1 @@
|
||||
<img class="inline-flex w-4 h-4" src="{{ asset('svgs/internal-link.svg') }}">
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="flex flex-col items-center justify-center h-screen">
|
||||
<span class="text-xl font-bold text-white">You have reached the limit of {{ $name }} you can create.</span>
|
||||
<span>Please <a class="text-white underline "href="{{ route('team.index') }}">upgrade your
|
||||
<span>Please <a class="text-white underline "href="{{ route('subscription.show') }}">upgrade your
|
||||
subscription</a> to create more
|
||||
{{ $name }}.</span>
|
||||
</div>
|
||||
|
||||
@@ -44,9 +44,10 @@
|
||||
<div>
|
||||
<button x-on:click.prevent="open = !open" x-on:click.away="open = false" type="button"
|
||||
class="py-4 mx-4" id="menu-button" aria-expanded="true" aria-haspopup="true">
|
||||
<svg class="icon" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="currentColor"
|
||||
d="M224 128a8 8 0 0 1-8 8h-80v80a8 8 0 0 1-16 0v-80H40a8 8 0 0 1 0-16h80V40a8 8 0 0 1 16 0v80h80a8 8 0 0 1 8 8" />
|
||||
<svg class="icon text-neutral-400" xmlns="http://www.w3.org/2000/svg" width="200" height="200"
|
||||
viewBox="0 0 24 24">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
@@ -131,11 +132,24 @@
|
||||
<path d="M5 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
|
||||
<path d="M3 13v-1a2 2 0 0 1 2 -2h2" />
|
||||
</svg>
|
||||
Teams @if (isCloud())
|
||||
/ Subscription
|
||||
@endif
|
||||
Teams
|
||||
</a>
|
||||
</li>
|
||||
@if (isCloud())
|
||||
<li title="Subscription" class="hover:bg-coolgray-200">
|
||||
<a class="hover:bg-transparent hover:no-underline"
|
||||
href="{{ route('subscription.show') }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
class="{{ request()->is('subscription*') ? 'text-warning icon' : 'icon' }}"
|
||||
viewBox="0 0 24 24">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2"
|
||||
d="M3 8a3 3 0 0 1 3-3h12a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3zm0 2h18M7 15h.01M11 15h2" />
|
||||
</svg>
|
||||
Subscription
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
|
||||
@if (isInstanceAdmin())
|
||||
<li title="Settings" class="hover:bg-coolgray-200">
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 rounded bg-coolgray-400">
|
||||
{{-- <div class="p-4 rounded bg-coolgray-400">
|
||||
<h2 id="tier-hobby" class="flex items-start gap-4 text-4xl font-bold tracking-tight">Unlimited Trial
|
||||
<x-forms.button><a class="font-bold text-white hover:no-underline"
|
||||
href="https://github.com/coollabsio/coolify">Get Started</a></x-forms.button>
|
||||
@@ -42,8 +42,11 @@
|
||||
<p class="mt-4 text-sm leading-6">Start self-hosting <span class="text-warning">without limits</span> with
|
||||
our
|
||||
OSS version. Same features as the paid version, but you have to manage by yourself.</p>
|
||||
</div>
|
||||
</div> --}}
|
||||
|
||||
<div class="flow-root mt-12">
|
||||
<div class="pb-10 text-xl text-center">For the detailed list of features, please visit our landing page: <a
|
||||
class="font-bold text-white underline" href="https://coolify.io">coolify.io</a></div>
|
||||
<div
|
||||
class="grid max-w-sm grid-cols-1 -mt-16 divide-y divide-coolgray-500 isolate gap-y-16 sm:mx-auto lg:-mx-8 lg:mt-0 lg:max-w-none lg:grid-cols-3 lg:divide-x lg:divide-y-0 xl:-mx-4">
|
||||
|
||||
@@ -70,21 +73,18 @@
|
||||
{{ $basic }}
|
||||
@endisset
|
||||
@endif
|
||||
<p class="mt-10 text-sm leading-6 text-white h-[6.5rem]">Start self-hosting in
|
||||
the cloud
|
||||
with a
|
||||
single
|
||||
server.
|
||||
<p class="mt-10 text-sm leading-6 text-white h-[6.5rem]">Begin hosting your own services in the
|
||||
cloud.
|
||||
</p>
|
||||
<ul role="list" class="space-y-3 text-sm leading-6 ">
|
||||
<li class="flex gap-x-3">
|
||||
<svg class="flex-none w-5 h-6 text-warning" viewBox="0 0 20 20" fill="currentColor"
|
||||
<li class="flex">
|
||||
<svg class="flex-none w-5 h-6 mr-3 text-warning" viewBox="0 0 20 20" fill="currentColor"
|
||||
aria-hidden="true">
|
||||
<path fill-rule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
2 servers <x-helper helper="Bring Your Own Server." />
|
||||
Connect <span class="px-1 font-bold text-white">2</span> servers
|
||||
</li>
|
||||
<li class="flex gap-x-3">
|
||||
<svg class="flex-none w-5 h-6 text-warning" viewBox="0 0 20 20" fill="currentColor"
|
||||
@@ -141,17 +141,18 @@
|
||||
{{ $pro }}
|
||||
@endisset
|
||||
@endif
|
||||
<p class="h-20 mt-10 text-sm leading-6 text-white">Scale your business or self-hosting environment.
|
||||
<p class="h-20 mt-10 text-sm leading-6 text-white">Expand your business or set up your own hosting
|
||||
environment.
|
||||
</p>
|
||||
<ul role="list" class="mt-6 space-y-3 text-sm leading-6 ">
|
||||
<li class="flex gap-x-3">
|
||||
<svg class="flex-none w-5 h-6 text-warning" viewBox="0 0 20 20" fill="currentColor"
|
||||
<li class="flex ">
|
||||
<svg class="flex-none w-5 h-6 mr-3 text-warning" viewBox="0 0 20 20" fill="currentColor"
|
||||
aria-hidden="true">
|
||||
<path fill-rule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
10 servers <x-helper helper="Bring Your Own Server." />
|
||||
Connect <span class="px-1 font-bold text-white">10</span> servers
|
||||
</li>
|
||||
<li class="flex gap-x-3">
|
||||
<svg class="flex-none w-5 h-6 text-warning" viewBox="0 0 20 20" fill="currentColor"
|
||||
@@ -187,38 +188,38 @@
|
||||
</div>
|
||||
<div class="pt-16 lg:px-8 lg:pt-0 xl:px-14">
|
||||
<h3 id="tier-ultimate" class="text-base font-semibold leading-7 text-white">Ultimate</h3>
|
||||
<p class="flex items-baseline mt-6 gap-x-1">
|
||||
<p class="flex items-baseline mt-6 gap-x-1">
|
||||
<span x-show="selected === 'monthly'" x-cloak>
|
||||
<span class="text-4xl font-bold tracking-tight text-white">$?</span>
|
||||
<span class="text-sm font-semibold leading-6 ">/month + VAT</span>
|
||||
<span class="text-4xl font-bold tracking-tight text-white">Custom</span>
|
||||
{{-- <span class="text-sm font-semibold leading-6 ">pay-as-you-go</span> --}}
|
||||
</span>
|
||||
<span x-show="selected === 'yearly'" x-cloak>
|
||||
<span class="text-4xl font-bold tracking-tight text-white">$?</span>
|
||||
<span class="text-sm font-semibold leading-6 ">/month + VAT</span>
|
||||
<span class="text-4xl font-bold tracking-tight text-white">Custom</span>
|
||||
{{-- <span class="text-sm font-semibold leading-6 ">/month + VAT</span> --}}
|
||||
</span>
|
||||
</p>
|
||||
<span x-show="selected === 'monthly'" x-cloak>
|
||||
<span>billed monthly</span>
|
||||
<span x-show="selected === 'monthly'" x-cloak>
|
||||
<span>pay-as-you-go</span>
|
||||
</span>
|
||||
<span x-show="selected === 'yearly'" x-cloak>
|
||||
<span>billed annually</span>
|
||||
<span>pay-as-you-go</span>
|
||||
</span>
|
||||
@if ($showSubscribeButtons)
|
||||
@isset($ultimate)
|
||||
{{ $ultimate }}
|
||||
@endisset
|
||||
@endif
|
||||
<p class="h-20 mt-10 text-sm leading-6 text-white">Deploy complex infrastructures and
|
||||
manage them easily in one place.</p>
|
||||
<p class="h-20 mt-10 text-sm leading-6 text-white">Easily manage complex infrastructures in a
|
||||
single location.</p>
|
||||
<ul role="list" class="mt-6 space-y-3 text-sm leading-6 ">
|
||||
<li class="flex gap-x-3">
|
||||
<svg class="flex-none w-5 h-6 text-warning" viewBox="0 0 20 20" fill="currentColor"
|
||||
<li class="flex ">
|
||||
<svg class="flex-none w-5 h-6 mr-3 text-warning" viewBox="0 0 20 20" fill="currentColor"
|
||||
aria-hidden="true">
|
||||
<path fill-rule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
? servers <x-helper helper="Bring Your Own Server." />
|
||||
Connect <span class="px-1 font-bold text-white">10+</span> servers
|
||||
</li>
|
||||
|
||||
<li class="flex gap-x-3">
|
||||
@@ -254,7 +255,7 @@
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 mt-10 rounded">
|
||||
{{-- <div class="p-4 mt-10 rounded">
|
||||
<div class="flex items-start gap-4 text-xl tracking-tight">Need official support for
|
||||
your self-hosted instance?
|
||||
<x-forms.button>
|
||||
@@ -263,9 +264,10 @@
|
||||
Us</a>
|
||||
</x-forms.button>
|
||||
</div>
|
||||
</div>
|
||||
</div> --}}
|
||||
</div>
|
||||
<div class="pt-8 pb-12 text-4xl font-bold text-center text-white">Included in all plans</div>
|
||||
|
||||
{{-- <div class="pt-8 pb-12 text-4xl font-bold text-center text-white">Included in all plans</div>
|
||||
<div class="grid grid-cols-1 gap-10 md:grid-cols-2 gap-y-28">
|
||||
<div>
|
||||
<div class="flex items-center gap-4 mb-4">
|
||||
@@ -433,7 +435,7 @@
|
||||
</div>
|
||||
<div class="pt-20 text-xs">
|
||||
<span class="text-warning">*</span> Some features are work in progress and will be available soon.
|
||||
</div>
|
||||
</div> --}}
|
||||
</div>
|
||||
@isset($other)
|
||||
{{ $other }}
|
||||
|
||||
@@ -1,24 +1,18 @@
|
||||
<div>
|
||||
@if ($server->isFunctional())
|
||||
<div class="flex h-full pr-4">
|
||||
<div class="flex flex-col w-48 gap-4 min-w-fit">
|
||||
<a class="{{ request()->routeIs('server.proxy') ? 'text-white' : '' }}"
|
||||
href="{{ route('server.proxy', $parameters) }}">
|
||||
<button>Configuration</button>
|
||||
</a>
|
||||
@if (data_get($server, 'proxy.type') !== 'NONE')
|
||||
<a class="{{ request()->routeIs('server.proxy.dynamic-confs') ? 'text-white' : '' }}"
|
||||
href="{{ route('server.proxy.dynamic-confs', $parameters) }}">
|
||||
<button>Dynamic Configurations</button>
|
||||
</a>
|
||||
<a class="{{ request()->routeIs('server.proxy.logs') ? 'text-white' : '' }}"
|
||||
href="{{ route('server.proxy.logs', $parameters) }}">
|
||||
<button>Logs</button>
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div>Server is not validated. Validate first.</div>
|
||||
@endif
|
||||
<div class="flex h-full pr-4">
|
||||
<div class="flex flex-col w-48 gap-4 min-w-fit">
|
||||
<a class="{{ request()->routeIs('server.proxy') ? 'text-white' : '' }}"
|
||||
href="{{ route('server.proxy', $parameters) }}">
|
||||
<button>Configuration</button>
|
||||
</a>
|
||||
@if (data_get($server, 'proxy.type') !== 'NONE')
|
||||
<a class="{{ request()->routeIs('server.proxy.dynamic-confs') ? 'text-white' : '' }}"
|
||||
href="{{ route('server.proxy.dynamic-confs', $parameters) }}">
|
||||
<button>Dynamic Configurations</button>
|
||||
</a>
|
||||
<a class="{{ request()->routeIs('server.proxy.logs') ? 'text-white' : '' }}"
|
||||
href="{{ route('server.proxy.logs', $parameters) }}">
|
||||
<button>Logs</button>
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<div class="navbar-main" x-data>
|
||||
<a class="{{ request()->routeIs('project.service.configuration') ? 'text-white' : '' }}"
|
||||
<a class="{{ request()->routeIs('project.service.configuration') ? 'text-white' : '' }}"
|
||||
href="{{ route('project.service.configuration', $parameters) }}">
|
||||
<button>Configuration</button>
|
||||
</a>
|
||||
<x-services.links />
|
||||
<div class="flex-1"></div>
|
||||
@if ($service->status() === 'degraded')
|
||||
@if (str($service->status())->contains('degraded'))
|
||||
<button wire:click='deploy' onclick="startService.showModal()"
|
||||
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
|
||||
<svg class="w-5 h-5 text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -26,11 +26,10 @@
|
||||
Stop
|
||||
</button>
|
||||
@endif
|
||||
@if ($service->status() === 'running')
|
||||
@if (str($service->status())->contains('running'))
|
||||
<button wire:click='restart' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
|
||||
<svg class="w-5 h-5 text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
|
||||
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
|
||||
<path d="M20 4v5h-5" />
|
||||
</g>
|
||||
@@ -47,7 +46,7 @@
|
||||
Stop
|
||||
</button>
|
||||
@endif
|
||||
@if ($service->status() === 'exited')
|
||||
@if (str($service->status())->contains('exited'))
|
||||
<button wire:click='stop(true)'
|
||||
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
|
||||
<svg class="w-5 h-5 " viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -70,9 +69,9 @@
|
||||
</div>
|
||||
|
||||
@script
|
||||
<script>
|
||||
$wire.on('image-pulled', () => {
|
||||
startService.showModal();
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
$wire.on('image-pulled', () => {
|
||||
startService.showModal();
|
||||
});
|
||||
</script>
|
||||
@endscript
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
'status' => 'Degraded',
|
||||
])
|
||||
<x-loading wire:loading.delay.longer />
|
||||
<div class="flex items-center gap-2" wire:loading.remove.delay.longer>
|
||||
<div class="flex items-center" wire:loading.remove.delay.longer>
|
||||
<div class="badge badge-warning badge-xs"></div>
|
||||
<div class="text-xs font-medium tracking-wide text-warning">{{ Str::headline($status) }}</div>
|
||||
<div class="pl-2 pr-1 text-xs font-bold tracking-widerr text-warning">
|
||||
{{ str($status)->before(':')->headline() }}
|
||||
</div>
|
||||
@if (!str($status)->startsWith('Proxy') && !str($status)->contains('('))
|
||||
<div class="text-xs text-warning">({{ str($status)->after(':') }})</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
@if (str($status)->startsWith('running'))
|
||||
<x-status.running :status="$status" />
|
||||
@elseif(str($status)->startsWith('restarting') ||
|
||||
str($status)->startsWith('starting') ||
|
||||
str($status)->startsWith('degraded'))
|
||||
<x-status.restarting :status="$status" />
|
||||
@if (str($resource->status)->startsWith('running'))
|
||||
<x-status.running :status="$resource->status" />
|
||||
@elseif(str($resource->status)->startsWith('restarting') ||
|
||||
str($resource->status)->startsWith('starting') ||
|
||||
str($resource->status)->startsWith('degraded'))
|
||||
<x-status.restarting :status="$resource->status" />
|
||||
@else
|
||||
<x-status.stopped :status="$status" />
|
||||
<x-status.stopped :status="$resource->status" />
|
||||
@endif
|
||||
|
||||
@if (!str($status)->contains('exited') && $showRefreshButton)
|
||||
@if (!str($resource->status)->contains('exited') && $showRefreshButton)
|
||||
<button title="Refresh Status" wire:click='check_status(true)' class="mx-1 hover:fill-white fill-warning">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
'status' => 'Restarting',
|
||||
])
|
||||
<x-loading wire:loading.delay.longer />
|
||||
<div class="flex items-center " wire:loading.remove.delay.longer>
|
||||
<div class="flex items-center" wire:loading.remove.delay.longer>
|
||||
<div class="badge badge-warning badge-xs"></div>
|
||||
<div class="pl-2 pr-1 text-xs font-bold tracking-widerr text-warning">
|
||||
{{ str($status)->before(':')->headline() }}
|
||||
|
||||
@@ -3,5 +3,5 @@ We would like to inform you that a {{ config('constants.limits.trial_period') }}
|
||||
|
||||
You can try out Coolify, without payment information for free. If you like it, you can upgrade to a paid plan at any time.
|
||||
|
||||
[Click here](https://app.coolify.io/subscription) to start your trial.
|
||||
[Click here](https://app.coolify.io/subscription/new) to start your trial.
|
||||
</x-emails.layout>
|
||||
|
||||
19
resources/views/emails/daily-backup.blade.php
Normal file
19
resources/views/emails/daily-backup.blade.php
Normal 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>
|
||||
5
resources/views/emails/server-force-disabled.blade.php
Normal file
5
resources/views/emails/server-force-disabled.blade.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<x-emails.layout>
|
||||
Your server ({{ $name }}) disabled because it is not paid! All automations and integrations are stopped.
|
||||
|
||||
Please update your subscription to enable the server again [here](https://app.coolify.io/subsciprtions).
|
||||
</x-emails.layout>
|
||||
3
resources/views/emails/server-force-enabled.blade.php
Normal file
3
resources/views/emails/server-force-enabled.blade.php
Normal file
@@ -0,0 +1,3 @@
|
||||
<x-emails.layout>
|
||||
Your server ({{ $name }}) is enabled again!
|
||||
</x-emails.layout>
|
||||
@@ -1,5 +1,5 @@
|
||||
<x-emails.layout>
|
||||
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>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<magic-bar></magic-bar>
|
||||
</div>
|
||||
@endpersist
|
||||
<livewire:sponsorship />
|
||||
<livewire:layout-popups />
|
||||
@auth
|
||||
<livewire:realtime-connection />
|
||||
@endauth
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
{{ auth()->user()->name }}
|
||||
<h3 class="pt-4">Users</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div class="w-96 box" wire:click="switchUser('0')">
|
||||
<div class="text-white cursor-pointer w-96 box-without-bg bg-coollabs-100" wire:click="switchUser('0')">
|
||||
Root
|
||||
</div>
|
||||
@foreach ($users as $user)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<h1>Dashboard</h1>
|
||||
<div class="subtitle">Your self-hosted environment</div>
|
||||
@if (request()->query->get('success'))
|
||||
<div class="text-white rounded alert alert-success">
|
||||
<div class="mb-10 text-white rounded alert alert-success">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 stroke-current shrink-0" fill="none"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
@@ -16,53 +16,52 @@
|
||||
</div>
|
||||
@endif
|
||||
@if ($projects->count() === 0 && $servers->count() === 0)
|
||||
No resources found. Add your first server / private key <a class="text-white underline"
|
||||
href="{{ route('server.create') }}">here</a>.
|
||||
No resources found. Add your first server & private key <a class="text-white underline"
|
||||
href="{{ route('server.create') }}">here</a> or go to the <a class="text-white underline" href="{{ route('boarding') }}">boarding page</a>.
|
||||
@endif
|
||||
@if ($projects->count() > 0)
|
||||
<h3 class="pb-4">Projects</h3>
|
||||
@endif
|
||||
@if ($projects->count() === 1)
|
||||
<div class="grid grid-cols-1 gap-2">
|
||||
@else
|
||||
<div class="grid grid-cols-1 gap-2 xl:grid-cols-2">
|
||||
@endif
|
||||
@foreach ($projects as $project)
|
||||
<div class="gap-2 border border-transparent cursor-pointer box group">
|
||||
@if (data_get($project, 'environments')->count() === 1)
|
||||
<a class="flex flex-col flex-1 mx-6 hover:no-underline"
|
||||
href="{{ route('project.resource.index', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => data_get($project, 'environments.0.name', 'production')]) }}">
|
||||
<div class="font-bold text-white">{{ $project->name }}</div>
|
||||
<div class="description">
|
||||
{{ $project->description }}</div>
|
||||
</a>
|
||||
@if ($projects->count() === 1)
|
||||
<div class="grid grid-cols-1 gap-2">
|
||||
@else
|
||||
<a class="flex flex-col flex-1 mx-6 hover:no-underline"
|
||||
href="{{ route('project.show', ['project_uuid' => data_get($project, 'uuid')]) }}">
|
||||
<div class="font-bold text-white">{{ $project->name }}</div>
|
||||
<div class="description">
|
||||
{{ $project->description }}</div>
|
||||
</a>
|
||||
@endif
|
||||
<div class="flex items-center">
|
||||
<a class="mx-4 rounded group-hover:text-white hover:no-underline"
|
||||
href="{{ route('project.resource.create', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => data_get($project, 'environments.0.name', 'production')]) }}">
|
||||
<span class="font-bold hover:text-warning">+ Add Resource</span>
|
||||
</a>
|
||||
<a class="mx-4 rounded group-hover:text-white"
|
||||
href="{{ route('project.edit', ['project_uuid' => data_get($project, 'uuid')]) }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon hover:text-warning" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z" />
|
||||
<path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" />
|
||||
</svg>
|
||||
</a>
|
||||
<div class="grid grid-cols-1 gap-2 xl:grid-cols-2">
|
||||
@endif
|
||||
@foreach ($projects as $project)
|
||||
<div class="gap-2 border border-transparent cursor-pointer box group">
|
||||
@if (data_get($project, 'environments')->count() === 1)
|
||||
<a class="flex flex-col flex-1 mx-6 hover:no-underline"
|
||||
href="{{ route('project.resource.index', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => data_get($project, 'environments.0.name', 'production')]) }}">
|
||||
<div class="font-bold text-white">{{ $project->name }}</div>
|
||||
<div class="description">
|
||||
{{ $project->description }}</div>
|
||||
</a>
|
||||
@else
|
||||
<a class="flex flex-col flex-1 mx-6 hover:no-underline"
|
||||
href="{{ route('project.show', ['project_uuid' => data_get($project, 'uuid')]) }}">
|
||||
<div class="font-bold text-white">{{ $project->name }}</div>
|
||||
<div class="description">
|
||||
{{ $project->description }}</div>
|
||||
</a>
|
||||
@endif
|
||||
<div class="flex items-center">
|
||||
<a class="mx-4 rounded group-hover:text-white hover:no-underline"
|
||||
href="{{ route('project.resource.create', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => data_get($project, 'environments.0.name', 'production')]) }}">
|
||||
<span class="font-bold hover:text-warning">+ Add Resource</span>
|
||||
</a>
|
||||
<a class="mx-4 rounded group-hover:text-white"
|
||||
href="{{ route('project.edit', ['project_uuid' => data_get($project, 'uuid')]) }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon hover:text-warning" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z" />
|
||||
<path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
@endforeach
|
||||
</div>
|
||||
@if ($projects->count() > 0)
|
||||
<h3 class="py-4">Servers</h3>
|
||||
@@ -139,6 +138,7 @@
|
||||
<div>No deployments running.</div>
|
||||
@endforelse
|
||||
</div>
|
||||
@endif
|
||||
<script>
|
||||
function gotoProject(uuid, environment = 'production') {
|
||||
window.location.href = '/project/' + uuid + '/' + environment;
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
<button class="text-white btn-link">{{ data_get($docker, 'network') }} </button>
|
||||
</a>
|
||||
@empty
|
||||
<div class="">N/A</div>
|
||||
@endforelse
|
||||
@forelse ($server->swarmDockers as $docker)
|
||||
<a
|
||||
@@ -24,7 +23,6 @@
|
||||
<button class="text-white btn-link">{{ data_get($docker, 'network') }} </button>
|
||||
</a>
|
||||
@empty
|
||||
<div class="">N/A</div>
|
||||
@endforelse
|
||||
</div>
|
||||
<div class="pt-2">
|
||||
|
||||
@@ -11,4 +11,13 @@
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@if (currentTeam()->serverOverflow())
|
||||
<x-banner :closable=false>
|
||||
<div><span class="font-bold text-red-500">WARNING:</span> The number of active servers exceeds the limit
|
||||
covered by your payment. If not resolved, some of your servers <span class="font-bold text-red-500">will
|
||||
be deactivated</span>. Visit <a href="{{ route('subscription.show') }}"
|
||||
class="text-white underline">/subscription</a> to update your subscription or remove some servers.
|
||||
</div>
|
||||
</x-banner>
|
||||
@endif
|
||||
</div>
|
||||
@@ -4,8 +4,8 @@
|
||||
<h2>Advanced</h2>
|
||||
</div>
|
||||
<div>Advanced configuration for your application.</div>
|
||||
<div class="flex flex-col pt-4 ">
|
||||
<h4>General</h4>
|
||||
<div class="flex flex-col pt-4 w-96">
|
||||
<h3>General</h3>
|
||||
@if ($application->git_based())
|
||||
<x-forms.checkbox helper="Automatically deploy new commits based on Git webhooks." instantSave
|
||||
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>"
|
||||
instantSave id="application.settings.is_consistent_container_name_enabled"
|
||||
label="Consistent Container Names" />
|
||||
<h4>Logs</h4>
|
||||
<h3>Logs</h3>
|
||||
@if (!$application->settings->is_raw_compose_deployment_enabled)
|
||||
<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" />
|
||||
@endif
|
||||
|
||||
@if ($application->git_based())
|
||||
<h4>Git</h4>
|
||||
<x-forms.checkbox instantSave id="application.settings.is_git_submodules_enabled" label="Git Submodules"
|
||||
<h3>Git</h3>
|
||||
<x-forms.checkbox instantSave id="application.settings.is_git_submodules_enabled" label="Submodules"
|
||||
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." />
|
||||
@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_custom_ssl" label="Is Custom SSL?" />
|
||||
<x-forms.checkbox disabled instantSave id="is_http2" label="Is Http2?" /> --}}
|
||||
</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>
|
||||
|
||||
@@ -27,10 +27,17 @@
|
||||
<a :class="activeTab === 'source' && 'text-white'"
|
||||
@click.prevent="activeTab = 'source'; window.location.hash = 'source'" href="#">Source</a>
|
||||
@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
|
||||
@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 :class="activeTab === 'scheduled-tasks' && 'text-white'"
|
||||
@click.prevent="activeTab = 'scheduled-tasks'; window.location.hash = 'scheduled-tasks'"
|
||||
href="#">Scheduled Tasks
|
||||
|
||||
@@ -52,8 +52,8 @@
|
||||
@foreach (decode_remote_command_output($application_deployment_queue) as $line)
|
||||
<div @class([
|
||||
'font-mono',
|
||||
'text-warning' => $line['hidden'],
|
||||
'text-red-500' => $line['type'] == 'stderr',
|
||||
'text-warning whitespace-pre-line' => $line['hidden'],
|
||||
'text-red-500 whitespace-pre-line' => $line['type'] == 'stderr',
|
||||
])>[{{ $line['timestamp'] }}] @if ($line['hidden'])
|
||||
<br>COMMAND: <br>{{ $line['command'] }} <br><br>OUTPUT:
|
||||
@endif @if (str($line['output'])->contains('http://') || str($line['output'])->contains('https://'))
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
@if ($isConfigurationChanged && !is_null($application->config_hash) && !$application->isExited())
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
@endif
|
||||
@@ -100,11 +101,15 @@
|
||||
<x-forms.input id="application.docker_registry_image_tag" label="Docker Image Tag" />
|
||||
@endif
|
||||
@else
|
||||
@if ($application->destination->server->isSwarm() || $application->additional_servers->count() > 0)
|
||||
<x-forms.input id="application.docker_registry_image_name" required label="Docker Image" />
|
||||
@if (
|
||||
$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"
|
||||
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
|
||||
<x-forms.input id="application.docker_registry_image_name"
|
||||
helper="Empty means it won't push the image to a docker registry."
|
||||
|
||||
@@ -86,15 +86,21 @@
|
||||
Redeploy
|
||||
@endif
|
||||
</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
|
||||
href="{{ route('project.application.deployment.index', [...$parameters, 'pull_request_id' => data_get($preview, 'pull_request_id')]) }}">
|
||||
<x-forms.button class="bg-coolgray-500">
|
||||
Get Deployment Logs
|
||||
Deployment Logs
|
||||
</x-forms.button>
|
||||
</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>
|
||||
@endforeach
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<nav wire:poll.30000ms="check_status">
|
||||
<nav wire:poll.5000ms="check_status">
|
||||
<x-resources.breadcrumbs :resource="$database" :parameters="$parameters" />
|
||||
<x-databases.navbar :database="$database" :parameters="$parameters" />
|
||||
</nav>
|
||||
|
||||
@@ -168,7 +168,7 @@
|
||||
<x-forms.button wire:click="loadServices('force')">Reload List</x-forms.button>
|
||||
<input
|
||||
class="w-full text-white rounded input input-sm bg-coolgray-200 disabled:bg-coolgray-200/50 disabled:border-none placeholder:text-coolgray-500 read-only:text-neutral-500 read-only:bg-coolgray-200/50"
|
||||
wire:model.live.debounce.200ms="search" placeholder="Search...">
|
||||
wire:model.live.debounce.200ms="search" autofocus placeholder="Search...">
|
||||
</div>
|
||||
<div class="grid justify-start grid-cols-1 gap-4 text-left xl:grid-cols-3">
|
||||
@if ($loadingServices)
|
||||
@@ -176,28 +176,29 @@
|
||||
@else
|
||||
@forelse ($services as $serviceName => $service)
|
||||
@if (data_get($service, 'minversion') && version_compare(config('version'), data_get($service, 'minversion'), '<'))
|
||||
<x-resource-view wire="setType('one-click-service-{{ $serviceName }}')">
|
||||
<x-slot:title> {{ Str::headline($serviceName) }}</x-slot>
|
||||
<x-slot:description>
|
||||
@if (data_get($service, 'slogan'))
|
||||
{{ data_get($service, 'slogan') }}
|
||||
@endif
|
||||
<x-resource-view wire="setType('one-click-service-{{ $serviceName }}')">
|
||||
<x-slot:title> {{ Str::headline($serviceName) }}</x-slot>
|
||||
<x-slot:description>
|
||||
@if (data_get($service, 'slogan'))
|
||||
{{ data_get($service, 'slogan') }}
|
||||
@endif
|
||||
|
||||
</x-slot>
|
||||
<x-slot:logo>
|
||||
@if (data_get($service, 'logo'))
|
||||
<img class="w-[4.5rem]
|
||||
</x-slot>
|
||||
<x-slot:logo>
|
||||
@if (data_get($service, 'logo'))
|
||||
<img class="w-[4.5rem]
|
||||
aspect-square h-[4.5rem] p-2 transition-all duration-200 opacity-30 grayscale group-hover:grayscale-0 group-hover:opacity-100"
|
||||
src="{{ asset(data_get($service, 'logo')) }}">
|
||||
@endif
|
||||
</x-slot:logo>
|
||||
<x-slot:documentation>
|
||||
{{ data_get($service, 'documentation') }}
|
||||
</x-slot>
|
||||
<x-slot:upgrade>
|
||||
You need to upgrade Coolify to {{ data_get($service, 'minversion') }} to use this service.
|
||||
</x-slot>
|
||||
</x-resource-view>
|
||||
src="{{ asset(data_get($service, 'logo')) }}">
|
||||
@endif
|
||||
</x-slot:logo>
|
||||
<x-slot:documentation>
|
||||
{{ data_get($service, 'documentation') }}
|
||||
</x-slot>
|
||||
<x-slot:upgrade>
|
||||
You need to upgrade Coolify to {{ data_get($service, 'minversion') }} to use this
|
||||
service.
|
||||
</x-slot>
|
||||
</x-resource-view>
|
||||
{{-- <button class="text-left cursor-not-allowed bg-coolgray-100 box-without-bg" disabled>
|
||||
<div class="flex flex-col mx-6">
|
||||
<div class="font-bold">
|
||||
@@ -215,10 +216,14 @@
|
||||
@endif
|
||||
</x-slot>
|
||||
<x-slot:logo>
|
||||
@if (data_get($service, 'logo'))
|
||||
@if (file_exists(public_path(data_get($service, 'logo'))))
|
||||
<img class="w-[4.5rem]
|
||||
aspect-square h-[4.5rem] p-2 transition-all duration-200 opacity-30 grayscale group-hover:grayscale-0 group-hover:opacity-100"
|
||||
aspect-square h-[4.5rem] p-2 transition-all duration-200 opacity-30 grayscale group-hover:grayscale-0 group-hover:opacity-100"
|
||||
src="{{ asset(data_get($service, 'logo')) }}">
|
||||
@else
|
||||
<img class="w-[4.5rem]
|
||||
aspect-square h-[4.5rem] p-2 transition-all duration-200 opacity-30 grayscale group-hover:grayscale-0 group-hover:opacity-100"
|
||||
src="{{ asset('svgs/unknown.svg') }}">
|
||||
@endif
|
||||
</x-slot:logo>
|
||||
<x-slot:documentation>
|
||||
|
||||
@@ -45,12 +45,12 @@
|
||||
class="items-center justify-center box">+ Add New Resource</a>
|
||||
@else
|
||||
<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">
|
||||
<template x-for="item in filteredApplications" :key="item.id">
|
||||
<span>
|
||||
<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="pb-2 font-bold text-white" x-text="item.name"></div>
|
||||
<template x-if="item.status.startsWith('running')">
|
||||
@@ -66,8 +66,8 @@
|
||||
<div title="degraded" class="mt-1 bg-warning badge badge-xs"></div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="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.description"></div>
|
||||
<div class="max-w-full truncate description" x-text="item.fqdn"></div>
|
||||
</div>
|
||||
</a>
|
||||
<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">
|
||||
<span>
|
||||
<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="pb-2 font-bold text-white" x-text="item.name"></div>
|
||||
<template x-if="item.status.startsWith('running')">
|
||||
@@ -99,7 +99,7 @@
|
||||
<div title="degraded" class="mt-1 bg-warning badge badge-xs"></div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="description" x-text="item.description"></div>
|
||||
<div class="max-w-full truncate description" x-text="item.description"></div>
|
||||
</div>
|
||||
</a>
|
||||
<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">
|
||||
<span>
|
||||
<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="pb-2 font-bold text-white" x-text="item.name"></div>
|
||||
<template x-if="item.status.startsWith('running')">
|
||||
@@ -131,7 +131,7 @@
|
||||
<div title="degraded" class="mt-1 bg-warning badge badge-xs"></div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="description" x-text="item.description"></div>
|
||||
<div class="max-w-full truncate description" x-text="item.description"></div>
|
||||
</div>
|
||||
</a>
|
||||
<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">
|
||||
<span>
|
||||
<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="pb-2 font-bold text-white" x-text="item.name"></div>
|
||||
<template x-if="item.status.startsWith('running')">
|
||||
@@ -163,7 +163,7 @@
|
||||
<div title="degraded" class="mt-1 bg-warning badge badge-xs"></div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="description" x-text="item.description"></div>
|
||||
<div class="max-w-full truncate description" x-text="item.description"></div>
|
||||
</div>
|
||||
</a>
|
||||
<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">
|
||||
<span>
|
||||
<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="pb-2 font-bold text-white" x-text="item.name"></div>
|
||||
<template x-if="item.status.startsWith('running')">
|
||||
@@ -195,7 +195,7 @@
|
||||
<div title="degraded" class="mt-1 bg-warning badge badge-xs"></div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="description" x-text="item.description"></div>
|
||||
<div class="max-w-full truncate description" x-text="item.description"></div>
|
||||
</div>
|
||||
</a>
|
||||
<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">
|
||||
<span>
|
||||
<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="pb-2 font-bold text-white" x-text="item.name"></div>
|
||||
<template x-if="item.status.startsWith('running')">
|
||||
@@ -227,7 +227,7 @@
|
||||
<div title="degraded" class="mt-1 bg-warning badge badge-xs"></div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="description" x-text="item.description"></div>
|
||||
<div class="max-w-full truncate description" x-text="item.description"></div>
|
||||
</div>
|
||||
</a>
|
||||
<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">
|
||||
<span>
|
||||
<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="pb-2 font-bold text-white" x-text="item.name"></div>
|
||||
<template x-if="item.status.startsWith('running')">
|
||||
@@ -259,7 +259,7 @@
|
||||
<div title="degraded" class="mt-1 bg-warning badge badge-xs"></div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="description" x-text="item.description"></div>
|
||||
<div class="max-w-full truncate description" x-text="item.description"></div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="flex gap-1 pt-1 group-hover:text-white group min-h-6">
|
||||
|
||||
@@ -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" />
|
||||
<div class="flex h-full pt-6">
|
||||
<div class="flex flex-col items-start gap-4 min-w-fit">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user