Compare commits

...

33 Commits

Author SHA1 Message Date
Andras Bacsai
f93317cd2e Merge pull request #1224 from coollabsio/next
v4.0.0-beta.36
2023-09-14 16:28:52 +02:00
Andras Bacsai
8412802f4d oh wow, it is cool! 2023-09-14 15:52:04 +02:00
Andras Bacsai
53c20e1e99 feat: new container status checks 2023-09-14 12:45:50 +02:00
Andras Bacsai
3c8c8e20b1 fix: editable ip
fix: traefik dashboard
2023-09-14 11:37:20 +02:00
Andras Bacsai
49f8abcd79 remove unnecessary things 2023-09-14 10:56:25 +02:00
Andras Bacsai
4a4d73b87b fix: plus boarding step about Coolify 2023-09-14 10:39:05 +02:00
Andras Bacsai
046eab3776 fix: processWithEnv()->run 2023-09-14 10:26:48 +02:00
Andras Bacsai
17c0e91a0d feat: ssh-agent instead of filesystem based ssh keys 2023-09-14 10:12:58 +02:00
Andras Bacsai
fe4a0ae166 fix: encrypt jobs 2023-09-14 10:12:44 +02:00
Andras Bacsai
52c84f8d22 fix: lower case email on waitlist 2023-09-13 20:48:13 +02:00
Andras Bacsai
b22fecb615 fix: lowercase email in forgot password 2023-09-13 20:27:58 +02:00
Andras Bacsai
23b7fc3c54 fix: prevent weird ui bug for validateServer 2023-09-13 13:00:43 +02:00
Andras Bacsai
1efb1235b4 fix: add timeout for ssh commands 2023-09-13 13:00:16 +02:00
Andras Bacsai
0924070a13 version++ 2023-09-13 12:42:59 +02:00
Andras Bacsai
12fc5a8f91 Merge pull request #1223 from coollabsio/next
v4.0.0-beta.35
2023-09-13 12:34:45 +02:00
Andras Bacsai
9eba058cf7 fix: disable dockerfile based healtcheck for now 2023-09-13 12:08:44 +02:00
Andras Bacsai
fa4f5fea8c fix: forgot password 2023-09-13 11:29:31 +02:00
Andras Bacsai
898563fe7c email: server lost connection 2023-09-13 11:05:10 +02:00
Andras Bacsai
c418a17161 ui: show trial instead of sub 2023-09-12 17:24:46 +02:00
Andras Bacsai
cd0da04ea2 wip: server check instead of app check 2023-09-12 15:47:30 +02:00
Andras Bacsai
01e942c6a0 fix: show help modal everywhere 2023-09-12 15:06:07 +02:00
Andras Bacsai
bb9abafa82 test gh runner 2023-09-12 15:00:05 +02:00
Andras Bacsai
d0cd926517 fix: sub type 2023-09-12 14:53:54 +02:00
Andras Bacsai
9baf0161c7 fix: help should send cc on email 2023-09-12 14:51:35 +02:00
Andras Bacsai
8ba18b2ce1 fix: confirm email before sending 2023-09-12 13:19:55 +02:00
Andras Bacsai
ab021ee535 fix: server is functional check 2023-09-12 13:14:01 +02:00
Andras Bacsai
c76a1b1ba5 fix: webhooks should not run if server is not functional 2023-09-12 13:10:39 +02:00
Andras Bacsai
6266a5e500 internal: trial emails 2023-09-12 12:03:17 +02:00
Andras Bacsai
5d27e89bfa feat: dynamic trial period 2023-09-12 11:23:31 +02:00
Andras Bacsai
6da4e78374 feat: trial 2023-09-12 11:19:21 +02:00
Andras Bacsai
be30651172 fix: remove nixpkgarchive from ui 2023-09-12 09:46:10 +02:00
Andras Bacsai
95764c2b76 fix: remove nixpkgarchive 2023-09-12 09:45:20 +02:00
Andras Bacsai
92a75685b5 version plus plus 2023-09-11 23:06:24 +02:00
94 changed files with 1136 additions and 294 deletions

View File

@@ -13,7 +13,7 @@ env:
jobs:
amd64:
runs-on: ubuntu-latest
runs-on: [self-hosted, x64]
steps:
- uses: actions/checkout@v3
- name: Login to ghcr.io
@@ -52,7 +52,7 @@ jobs:
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next-aarch64
merge-manifest:
runs-on: ubuntu-latest
runs-on: [self-hosted, x64]
permissions:
contents: read
packages: write

View File

@@ -10,9 +10,6 @@ use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Process;
use Spatie\Activitylog\Models\Activity;
const TIMEOUT = 3600;
const IDLE_TIMEOUT = 3600;
class RunRemoteProcess
{
public Activity $activity;
@@ -76,8 +73,7 @@ class RunRemoteProcess
$this->time_start = hrtime(true);
$status = ProcessStatus::IN_PROGRESS;
$processResult = Process::timeout(TIMEOUT)->idleTimeout(IDLE_TIMEOUT)->run($this->getCommand(), $this->handleOutput(...));
$processResult = processWithEnv()->forever()->run($this->getCommand(), $this->handleOutput(...));
if ($this->activity->properties->get('status') === ProcessStatus::ERROR->value) {
$status = ProcessStatus::ERROR;
@@ -108,11 +104,10 @@ class RunRemoteProcess
{
$user = $this->activity->getExtraProperty('user');
$server_ip = $this->activity->getExtraProperty('server_ip');
$private_key_location = $this->activity->getExtraProperty('private_key_location');
$port = $this->activity->getExtraProperty('port');
$command = $this->activity->getExtraProperty('command');
return generate_ssh_command($private_key_location, $server_ip, $user, $port, $command);
return generateSshCommand($server_ip, $user, $port, $command);
}
protected function handleOutput(string $type, string $output)

View File

@@ -8,7 +8,7 @@ use Spatie\Activitylog\Models\Activity;
class StartProxy
{
public function __invoke(Server $server): Activity
public function __invoke(Server $server, bool $async = true): Activity|string
{
$proxy_path = get_proxy_path();
$networks = collect($server->standaloneDockers)->map(function ($docker) {
@@ -26,8 +26,7 @@ class StartProxy
$docker_compose_yml_base64 = base64_encode($configuration);
$server->proxy->last_applied_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value;
$server->save();
$activity = remote_process([
$commands = [
"echo '####### Creating required Docker networks...'",
...$create_networks_command,
"cd $proxy_path",
@@ -44,8 +43,13 @@ class StartProxy
"echo '####### Starting coolify-proxy...'",
'docker compose up -d --remove-orphans',
"echo '####### Proxy installed successfully...'"
], $server);
return $activity;
];
if (!$async) {
instant_remote_process($commands, $server);
return 'OK';
} else {
$activity = remote_process($commands, $server);
return $activity;
}
}
}

View File

@@ -6,7 +6,9 @@ use App\Jobs\SendConfirmationForWaitlistJob;
use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Models\ScheduledDatabaseBackup;
use App\Models\Server;
use App\Models\StandalonePostgresql;
use App\Models\Team;
use App\Models\TeamInvitation;
use App\Models\User;
use App\Models\Waitlist;
@@ -24,30 +26,31 @@ use Illuminate\Notifications\Messages\MailMessage;
use Mail;
use Str;
use function Laravel\Prompts\confirm;
use function Laravel\Prompts\select;
use function Laravel\Prompts\text;
class TestEmail extends Command
class Emails extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'email:test';
protected $signature = 'emails';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Send a test email to the admin';
protected $description = 'Send out test / prod emails';
/**
* Execute the console command.
*/
private ?MailMessage $mail = null;
private string $email = 'andras.bacsai@protonmail.com';
private ?string $email = null;
public function handle()
{
$type = select(
@@ -62,9 +65,14 @@ class TestEmail extends Command
'invitation-link' => 'Invitation Link',
'waitlist-invitation-link' => 'Waitlist Invitation Link',
'waitlist-confirmation' => 'Waitlist Confirmation',
'realusers-before-trial' => 'REAL - Registered Users Before Trial without Subscription',
'realusers-server-lost-connection' => 'REAL - Server Lost Connection',
],
);
$this->email = text('Email Address to send to');
$emailsGathered = ['realusers-before-trial','realusers-server-lost-connection'];
if (!in_array($type, $emailsGathered)) {
$this->email = text('Email Address to send to');
}
set_transanctional_email_settings();
$this->mail = new MailMessage();
@@ -159,16 +167,73 @@ class TestEmail extends Command
$found = Waitlist::where('email', $this->email)->first();
if ($found) {
SendConfirmationForWaitlistJob::dispatch($this->email, $found->uuid);
} else {
throw new Exception('Waitlist not found');
}
break;
case 'realusers-before-trial':
$this->mail = new MailMessage();
$this->mail->view('emails.before-trial-conversion');
$this->mail->subject('Trial period has been added for all subscription plans.');
$teams = Team::doesntHave('subscription')->where('id', '!=', 0)->get();
if (!$teams || $teams->isEmpty()) {
echo 'No teams found.' . PHP_EOL;
return;
}
$emails = [];
foreach ($teams as $team) {
foreach ($team->members as $member) {
if ($member->email) {
$emails[] = $member->email;
}
}
}
$emails = array_unique($emails);
$this->info("Sending to " . count($emails) . " emails.");
foreach ($emails as $email) {
$this->info($email);
}
$confirmed = confirm('Are you sure?');
if ($confirmed) {
foreach ($emails as $email) {
$this->sendEmail($email);
}
}
break;
case 'realusers-server-lost-connection':
$serverId = text('Server Id');
$server = Server::find($serverId);
if (!$server) {
throw new Exception('Server not found');
}
$admins = [];
$members = $server->team->members;
foreach ($members as $member) {
if ($member->isAdmin()) {
$admins[] = $member->email;
}
}
$this->info('Sending to ' . count($admins) . ' admins.');
foreach ($admins as $admin) {
$this->info($admin);
}
$this->mail = new MailMessage();
$this->mail->view('emails.server-lost-connection', [
'name' => $server->name,
]);
$this->mail->subject('Action required: Server ' . $server->name . ' lost connection.');
foreach ($admins as $email) {
$this->sendEmail($email);
}
break;
}
}
private function sendEmail()
private function sendEmail(string $email = null)
{
if ($email) {
$this->email = $email;
}
Mail::send(
[],
[],
@@ -177,5 +242,6 @@ class TestEmail extends Command
->subject($this->mail->subject)
->html((string)$this->mail->render())
);
$this->info("Email sent to $this->email successfully. 📧");
}
}

View File

@@ -92,7 +92,6 @@ class WaitlistInvite extends Command
}
private function send_email()
{
ray($this->next_patient->email, $this->password);
$token = Crypt::encryptString("{$this->next_patient->email}@@@$this->password");
$loginLink = route('auth.link', ['token' => $token]);
$mail = new MailMessage();

View File

@@ -2,20 +2,15 @@
namespace App\Console;
use App\Enums\ProxyTypes;
use App\Jobs\ApplicationContainerStatusJob;
use App\Jobs\CheckResaleLicenseJob;
use App\Jobs\CleanupInstanceStuffsJob;
use App\Jobs\DatabaseBackupJob;
use App\Jobs\DatabaseContainerStatusJob;
use App\Jobs\DockerCleanupJob;
use App\Jobs\InstanceAutoUpdateJob;
use App\Jobs\ProxyContainerStatusJob;
use App\Models\Application;
use App\Jobs\ContainerStatusJob;
use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup;
use App\Models\Server;
use App\Models\StandalonePostgresql;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
@@ -24,38 +19,31 @@ class Kernel extends ConsoleKernel
protected function schedule(Schedule $schedule): void
{
if (isDev()) {
$schedule->command('horizon:snapshot')->everyMinute();
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute();
// $schedule->job(new ContainerStatusJob(Server::find(0)))->everyTenMinutes()->onOneServer();
// $schedule->command('horizon:snapshot')->everyMinute();
// $schedule->job(new CleanupInstanceStuffsJob)->everyMinute();
// $schedule->job(new CheckResaleLicenseJob)->hourly();
$schedule->job(new DockerCleanupJob)->everyOddHour();
// $schedule->job(new DockerCleanupJob)->everyOddHour();
// $this->instance_auto_update($schedule);
// $this->check_scheduled_backups($schedule);
$this->check_resources($schedule);
} else {
$schedule->command('horizon:snapshot')->everyFiveMinutes();
$schedule->job(new CleanupInstanceStuffsJob)->everyTenMinutes()->onOneServer();
$schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer();
$schedule->job(new DockerCleanupJob)->everyTenMinutes()->onOneServer();
}
$this->instance_auto_update($schedule);
$this->check_scheduled_backups($schedule);
$this->check_resources($schedule);
$this->check_proxies($schedule);
}
private function check_proxies($schedule)
{
$servers = Server::all()->where('settings.is_usable', true)->where('settings.is_reachable', true)->whereNotNull('proxy.type')->where('proxy.type', '!=', ProxyTypes::NONE->value);
foreach ($servers as $server) {
$schedule->job(new ProxyContainerStatusJob($server))->everyMinute()->onOneServer();
$this->instance_auto_update($schedule);
$this->check_scheduled_backups($schedule);
$this->check_resources($schedule);
}
}
private function check_resources($schedule)
{
$applications = Application::all();
foreach ($applications as $application) {
$schedule->job(new ApplicationContainerStatusJob($application))->everyMinute()->onOneServer();
}
$servers = Server::all()->where('settings.is_usable', true)->where('settings.is_reachable', true);
ray($servers);
$postgresqls = StandalonePostgresql::all();
foreach ($postgresqls as $postgresql) {
$schedule->job(new DatabaseContainerStatusJob($postgresql))->everyMinute()->onOneServer();
foreach ($servers as $server) {
$schedule->job(new ContainerStatusJob($server))->everyMinute()->onOneServer();
}
}
private function instance_auto_update($schedule)

View File

@@ -13,7 +13,6 @@ class CoolifyTaskArgs extends Data
{
public function __construct(
public string $server_ip,
public string $private_key_location,
public string $command,
public int $port,
public string $user,

View File

@@ -53,12 +53,13 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$this->remoteServerHost = 'coolify-testing-host';
}
}
public function welcome() {
public function explanation() {
if (isCloud()) {
return $this->setServerType('remote');
}
$this->currentState = 'select-server-type';
}
public function restartBoarding()
{
if ($this->createdServer) {

View File

@@ -30,7 +30,7 @@ class Help extends Component
try {
$this->rateLimit(1, 60);
$this->validate();
$subscriptionType = auth()->user()?->subscription?->type() ?? 'unknown';
$subscriptionType = auth()->user()?->subscription?->type() ?? 'Free';
$debug = "Route: {$this->path}";
$mail = new MailMessage();
$mail->view(
@@ -41,7 +41,7 @@ class Help extends Component
]
);
$mail->subject("[HELP - {$subscriptionType}]: {$this->subject}");
send_user_an_email($mail, 'hi@coollabs.io');
send_user_an_email($mail, 'hi@coollabs.io', auth()->user()?->email);
$this->emit('success', 'Your message has been sent successfully. We will get in touch with you as soon as possible.');
} catch (\Throwable $e) {
return general_error_handler($e, $this);

View File

@@ -48,7 +48,6 @@ class General extends Component
'application.ports_exposes' => 'required',
'application.ports_mappings' => 'nullable',
'application.dockerfile' => 'nullable',
'application.nixpkgsarchive' => 'nullable',
];
protected $validationAttributes = [
'application.name' => 'name',
@@ -67,7 +66,6 @@ class General extends Component
'application.ports_exposes' => 'Ports exposes',
'application.ports_mappings' => 'Ports mappings',
'application.dockerfile' => 'Dockerfile',
'application.nixpkgsarchive' => 'Nixpkgs archive',
];
public function instantSave()

View File

@@ -2,9 +2,8 @@
namespace App\Http\Livewire\Project\Application;
use App\Jobs\ApplicationContainerStatusJob;
use App\Jobs\ContainerStatusJob;
use App\Models\Application;
use App\Notifications\Application\StatusChanged;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
@@ -22,10 +21,11 @@ class Heading extends Component
public function check_status()
{
dispatch_sync(new ApplicationContainerStatusJob(
application: $this->application,
));
dispatch_sync(new ContainerStatusJob($this->application->destination->server));
$this->application->refresh();
$this->application->previews->each(function ($preview) {
$preview->refresh();
});
}
public function force_deploy_without_cache()

View File

@@ -3,6 +3,7 @@
namespace App\Http\Livewire\Project\Application;
use App\Jobs\ApplicationContainerStatusJob;
use App\Jobs\ContainerStatusJob;
use App\Models\Application;
use App\Models\ApplicationPreview;
use Illuminate\Support\Collection;
@@ -23,14 +24,6 @@ class Previews extends Component
$this->parameters = get_route_parameters();
}
public function loadStatus($pull_request_id)
{
dispatch(new ApplicationContainerStatusJob(
application: $this->application,
pullRequestId: $pull_request_id
));
}
public function load_prs()
{
try {

View File

@@ -3,8 +3,7 @@
namespace App\Http\Livewire\Project\Database;
use App\Actions\Database\StartPostgresql;
use App\Jobs\DatabaseContainerStatusJob;
use App\Notifications\Application\StatusChanged;
use App\Jobs\ContainerStatusJob;
use Livewire\Component;
class Heading extends Component
@@ -25,9 +24,7 @@ class Heading extends Component
public function check_status()
{
dispatch_sync(new DatabaseContainerStatusJob(
database: $this->database,
));
dispatch_sync(new ContainerStatusJob($this->database->destination->server));
$this->database->refresh();
}

View File

@@ -56,7 +56,7 @@ class Form extends Component
$this->uptime = $uptime;
$this->emit('success', 'Server is reachable!');
} else {
$this->emit('error', 'Server is not rachable');
$this->emit('error', 'Server is not reachable');
return;
}
if ($dockerVersion) {
@@ -88,17 +88,11 @@ class Form extends Component
public function submit()
{
$this->validate();
// $validation = Validator::make($this->server->toArray(), [
// 'ip' => [
// 'ip'
// ],
// ]);
// if ($validation->fails()) {
// foreach ($validation->errors()->getMessages() as $key => $value) {
// $this->addError("server.{$key}", $value[0]);
// }
// return;
// }
$uniqueIPs = Server::all()->pluck('ip')->toArray();
if (in_array($this->server->ip, $uniqueIPs)) {
$this->emit('error', 'IP address is already in use by another team.');
return;
}
$this->server->settings->wildcard_domain = $this->wildcard_domain;
$this->server->settings->cleanup_after_percentage = $this->cleanup_after_percentage;
$this->server->settings->save();

View File

@@ -58,7 +58,7 @@ class ByIp extends Component
{
$this->validate();
try {
if (!$this->private_key_id) {
if (is_null($this->private_key_id)) {
return $this->emit('error', 'You must select a private key');
}
$server = Server::create([

View File

@@ -17,7 +17,7 @@ class Status extends Component
public function getProxyStatus()
{
try {
if (data_get($this->server, 'settings.is_usable') && data_get($this->server, 'settings.is_reachable')) {
if ($this->server->isFunctional()) {
$container = getContainerStatus(server: $this->server, container_id: 'coolify-proxy');
$this->server->proxy->status = $container;
$this->server->save();

View File

@@ -14,6 +14,7 @@ class ShowPrivateKey extends Component
public function setPrivateKey($newPrivateKeyId)
{
try {
refresh_server_connection($this->server->privateKey);
$oldPrivateKeyId = $this->server->private_key_id;
$this->server->update([
'private_key_id' => $newPrivateKeyId
@@ -26,7 +27,7 @@ class ShowPrivateKey extends Component
'private_key_id' => $oldPrivateKeyId
]);
$this->server->refresh();
refresh_server_connection($this->server->privateKey);
refresh_server_connection($this->server->privateKey);
return general_error_handler($e, that: $this);
}
}

View File

@@ -2,7 +2,7 @@
namespace App\Http\Livewire\Settings;
use App\Jobs\ProxyContainerStatusJob;
use App\Jobs\ContainerStatusJob;
use App\Models\InstanceSettings as ModelsInstanceSettings;
use App\Models\Server;
use Livewire\Component;
@@ -124,7 +124,7 @@ class Configuration extends Component
];
}
$this->save_configuration_to_disk($traefik_dynamic_conf, $file);
dispatch(new ProxyContainerStatusJob($this->server));
dispatch(new ContainerStatusJob($this->server));
}
}

View File

@@ -8,8 +8,13 @@ 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 subscribeStripe($type)
{
$team = currentTeam();
Stripe::setApiKey(config('subscription.stripe_api_key'));
switch ($type) {
case 'basic-monthly':
@@ -50,10 +55,23 @@ class PricingPlans extends Component
'automatic_tax' => [
'enabled' => true,
],
'mode' => 'subscription',
'success_url' => route('dashboard', ['success' => true]),
'cancel_url' => route('subscription.index', ['cancelled' => true]),
];
if (!data_get($team,'subscription.stripe_trial_already_ended')) {
$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;
if ($customer) {
$payload['customer'] = $customer;

View File

@@ -6,6 +6,7 @@ use App\Jobs\SendConfirmationForWaitlistJob;
use App\Models\User;
use App\Models\Waitlist;
use Livewire\Component;
use Str;
class Index extends Component
{
@@ -46,7 +47,7 @@ class Index extends Component
return;
}
$waitlist = Waitlist::create([
'email' => $this->email,
'email' => Str::lower($this->email),
'type' => 'registration',
]);

View File

@@ -4,15 +4,15 @@ namespace App\Jobs;
use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Notifications\Application\StatusChanged;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ApplicationContainerStatusJob implements ShouldQueue, ShouldBeUnique
class ApplicationContainerStatusJob implements ShouldQueue, ShouldBeUnique, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

View File

@@ -17,10 +17,10 @@ use App\Notifications\Application\DeploymentSuccess;
use App\Traits\ExecuteRemoteCommand;
use Exception;
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;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
@@ -28,10 +28,8 @@ use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml;
use Throwable;
use Visus\Cuid2\Cuid2;
use Yosymfony\Toml\Toml;
use Yosymfony\Toml\TomlArray;
class ApplicationDeploymentJob implements ShouldQueue
class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, ExecuteRemoteCommand;
@@ -49,7 +47,6 @@ class ApplicationDeploymentJob implements ShouldQueue
private GithubApp|GitlabApp $source;
private StandaloneDocker|SwarmDocker $destination;
private Server $server;
private string $private_key_location;
private ApplicationPreview|null $preview = null;
private string $container_name;
@@ -92,7 +89,7 @@ class ApplicationDeploymentJob implements ShouldQueue
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
$this->container_name = generateApplicationContainerName($this->application->uuid, $this->pull_request_id);
$this->private_key_location = save_private_key_for_server($this->server);
addPrivateKeyToSshAgent($this->server);
$this->saved_outputs = collect();
// Set preview fqdn
@@ -122,6 +119,9 @@ class ApplicationDeploymentJob implements ShouldQueue
if ($containers->count() > 0) {
$this->currently_running_container_name = data_get($containers[0], 'Names');
}
if ($this->pull_request_id !== 0 && $this->pull_request_id !== null) {
$this->currently_running_container_name = $this->container_name;
}
$this->application_deployment_queue->update([
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
]);
@@ -135,7 +135,9 @@ class ApplicationDeploymentJob implements ShouldQueue
$this->deploy();
}
}
if ($this->application->fqdn) dispatch(new ProxyContainerStatusJob($this->server));
if ($this->server->isProxyShouldRun()) {
dispatch(new ContainerStatusJob($this->server));
}
$this->next(ApplicationDeploymentStatus::FINISHED->value);
} catch (Exception $e) {
ray($e);
@@ -270,6 +272,7 @@ class ApplicationDeploymentJob implements ShouldQueue
"echo 'Rolling update completed.'"
],
);
$this->application->update(['status' => 'running']);
break;
}
$counter++;
@@ -296,7 +299,11 @@ class ApplicationDeploymentJob implements ShouldQueue
// $this->generate_build_env_variables();
// $this->add_build_env_variables_to_dockerfile();
$this->build_image();
$this->rolling_update();
$this->stop_running_container();
$this->execute_remote_command(
["echo -n 'Starting preview deployment.'"],
[$this->execute_in_builder("docker compose --project-directory {$this->workdir} up -d >/dev/null"), "hidden" => true],
);
}
private function prepare_builder_image()
@@ -413,7 +420,6 @@ class ApplicationDeploymentJob implements ShouldQueue
private function generate_nixpacks_confs()
{
ray('nixpkgsarchive', $this->application->nixpkgsarchive);
$this->execute_remote_command(
[
"echo -n 'Generating nixpacks configuration.'",
@@ -422,13 +428,6 @@ class ApplicationDeploymentJob implements ShouldQueue
[$this->execute_in_builder("cp {$this->workdir}/.nixpacks/Dockerfile {$this->workdir}/Dockerfile")],
[$this->execute_in_builder("rm -f {$this->workdir}/.nixpacks/Dockerfile")]
);
// if ($this->application->nixpkgsarchive) {
// $this->execute_remote_command([
// $this->execute_in_builder("cat {$this->workdir}/nixpacks.toml"), "hidden" => true, "save" => 'nixpacks_toml'
// ]);
// }
}
private function nixpacks_build_cmd()
@@ -508,7 +507,7 @@ class ApplicationDeploymentJob implements ShouldQueue
$this->destination->network => [
'external' => true,
'name' => $this->destination->network,
'attachable' => true,
'attachable' => true
]
]
];
@@ -584,10 +583,15 @@ class ApplicationDeploymentJob implements ShouldQueue
private function set_labels_for_applications()
{
$appId = $this->application->id;
if ($this->pull_request_id !== 0) {
$appId = $appId . '-pr-' . $this->pull_request_id;
}
$labels = [];
$labels[] = 'coolify.managed=true';
$labels[] = 'coolify.version=' . config('version');
$labels[] = 'coolify.applicationId=' . $this->application->id;
$labels[] = 'coolify.applicationId=' . $appId;
$labels[] = 'coolify.type=application';
$labels[] = 'coolify.name=' . $this->application->name;
if ($this->pull_request_id !== 0) {
@@ -648,6 +652,10 @@ class ApplicationDeploymentJob implements ShouldQueue
private function generate_healthcheck_commands()
{
if ($this->application->dockerfile) {
// TODO: disabled HC because there are several ways to hc a simple docker image, hard to figure out a good way. Like some docker images (pocketbase) does not have curl.
return 'exit 0';
}
if (!$this->application->health_check_port) {
$this->application->health_check_port = $this->application->ports_exposes_array[0];
}

View File

@@ -6,12 +6,13 @@ use App\Enums\ProcessStatus;
use App\Models\Application;
use App\Models\ApplicationPreview;
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\SerializesModels;
class ApplicationPullRequestUpdateJob implements ShouldQueue
class ApplicationPullRequestUpdateJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

View File

@@ -4,12 +4,13 @@ namespace App\Jobs;
use App\Actions\License\CheckResaleLicense;
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\SerializesModels;
class CheckResaleLicenseJob implements ShouldQueue
class CheckResaleLicenseJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

View File

@@ -4,13 +4,14 @@ namespace App\Jobs;
use App\Models\Waitlist;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class CleanupInstanceStuffsJob implements ShouldQueue, ShouldBeUnique
class CleanupInstanceStuffsJob implements ShouldQueue, ShouldBeUnique, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

View File

@@ -0,0 +1,291 @@
<?php
namespace App\Jobs;
use App\Actions\Proxy\StartProxy;
use App\Models\ApplicationPreview;
use App\Models\Server;
use App\Notifications\Container\ContainerRestarted;
use App\Notifications\Container\ContainerStopped;
use App\Notifications\Server\Unreachable;
use Arr;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Str;
class ContainerStatusJob implements ShouldQueue, ShouldBeUnique, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 1;
public $timeout = 120;
public function __construct(public Server $server)
{
}
public function middleware(): array
{
return [new WithoutOverlapping($this->server->uuid)];
}
public function uniqueId(): string
{
return $this->server->uuid;
}
private function checkServerConnection() {
ray("Checking server connection to {$this->server->ip}");
$uptime = instant_remote_process(['uptime'], $this->server, false);
if (!is_null($uptime)) {
ray('Server is up');
return true;
}
}
public function handle(): void
{
try {
ray()->clearAll();
$serverUptimeCheckNumber = 0;
$serverUptimeCheckNumberMax = 5;
while (true) {
if ($serverUptimeCheckNumber >= $serverUptimeCheckNumberMax) {
$this->server->settings()->update(['is_reachable' => false]);
$this->server->team->notify(new Unreachable($this->server));
return;
}
$result = $this->checkServerConnection();
if ($result) {
break;
}
$serverUptimeCheckNumber++;
sleep(5);
}
$containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server);
$containers = format_docker_command_output_to_json($containers);
$applications = $this->server->applications();
$databases = $this->server->databases();
$previews = $this->server->previews();
if ($this->server->isProxyShouldRun()) {
$foundProxyContainer = $containers->filter(function ($value, $key) {
return data_get($value, 'Name') === '/coolify-proxy';
})->first();
if (!$foundProxyContainer) {
resolve(StartProxy::class)($this->server, false);
$this->server->team->notify(new ContainerRestarted('coolify-proxy', $this->server));
}
}
$foundApplications = [];
$foundApplicationPreviews = [];
$foundDatabases = [];
foreach ($containers as $container) {
$containerStatus = data_get($container, 'State.Status');
$labels = data_get($container, 'Config.Labels');
$labels = Arr::undot(format_docker_labels_to_json($labels));
$labelId = data_get($labels, 'coolify.applicationId');
ray($labelId);
if ($labelId) {
if (str_contains($labelId,'-pr-')) {
$previewId = (int) Str::after($labelId, '-pr-');
$applicationId = (int) Str::before($labelId, '-pr-');
$preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id',$previewId)->first();
if ($preview) {
$foundApplicationPreviews[] = $preview->id;
$statusFromDb = $preview->status;
if ($statusFromDb !== $containerStatus) {
$preview->update(['status' => $containerStatus]);
}
} else {
//Notify user that this container should not be there.
}
} else {
$application = $applications->where('id', $labelId)->first();
if ($application) {
$foundApplications[] = $application->id;
$statusFromDb = $application->status;
if ($statusFromDb !== $containerStatus) {
$application->update(['status' => $containerStatus]);
}
} else {
//Notify user that this container should not be there.
}
}
} else {
$uuid = data_get($labels, 'com.docker.compose.service');
if ($uuid) {
$database = $databases->where('uuid', $uuid)->first();
if ($database) {
$foundDatabases[] = $database->id;
$statusFromDb = $database->status;
if ($statusFromDb !== $containerStatus) {
$database->update(['status' => $containerStatus]);
}
} else {
// Notify user that this container should not be there.
}
}
}
}
$notRunningApplications = $applications->pluck('id')->diff($foundApplications);
foreach($notRunningApplications as $applicationId) {
$application = $applications->where('id', $applicationId)->first();
if ($application->status === 'exited') {
continue;
}
$application->update(['status' => 'exited']);
$name = data_get($application, 'name');
$fqdn = data_get($application, 'fqdn');
$containerName = $name ? "$name ($fqdn)" : $fqdn;
$project = data_get($application, 'environment.project');
$environment = data_get($application, 'environment');
$url = base_url() . '/project/' . $project->uuid . "/" . $environment->name . "/application/" . $application->uuid;
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
}
$notRunningApplicationPreviews = $previews->pluck('id')->diff($foundApplicationPreviews);
foreach ($notRunningApplicationPreviews as $previewId) {
$preview = $previews->where('id', $previewId)->first();
if ($preview->status === 'exited') {
continue;
}
$preview->update(['status' => 'exited']);
$name = data_get($preview, 'name');
$fqdn = data_get($preview, 'fqdn');
$containerName = $name ? "$name ($fqdn)" : $fqdn;
$project = data_get($preview, 'application.environment.project');
$environment = data_get($preview, 'application.environment');
$url = base_url() . '/project/' . $project->uuid . "/" . $environment->name . "/application/" . $preview->application->uuid;
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
}
$notRunningDatabases = $databases->pluck('id')->diff($foundDatabases);
foreach($notRunningDatabases as $database) {
$database = $databases->where('id', $database)->first();
if ($database->status === 'exited') {
continue;
}
$database->update(['status' => 'exited']);
$name = data_get($database, 'name');
$fqdn = data_get($database, 'fqdn');
$containerName = $name;
$project = data_get($database, 'environment.project');
$environment = data_get($database, 'environment');
$url = base_url() . '/project/' . $project->uuid . "/" . $environment->name . "/database/" . $database->uuid;
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
}
return;
foreach ($applications as $application) {
$uuid = data_get($application, 'uuid');
$id = data_get($application, 'id');
$foundContainer = $containers->filter(function ($value, $key) use ($id, $uuid) {
$labels = data_get($value, 'Config.Labels');
$labels = Arr::undot(format_docker_labels_to_json($labels));
$labelId = data_get($labels, 'coolify.applicationId');
if ($labelId == $id) {
return $value;
}
$isPR = Str::startsWith(data_get($value, 'Name'), "/$uuid");
$isPR = Str::contains(data_get($value, 'Name'), "-pr-");
if ($isPR) {
ray('is pr');
return false;
}
return $value;
})->first();
ray($foundContainer);
if ($foundContainer) {
$containerStatus = data_get($foundContainer, 'State.Status');
$databaseStatus = data_get($application, 'status');
if ($containerStatus !== $databaseStatus) {
$application->update(['status' => $containerStatus]);
}
} else {
$databaseStatus = data_get($application, 'status');
if ($databaseStatus !== 'exited') {
$application->update(['status' => 'exited']);
$name = data_get($application, 'name');
$fqdn = data_get($application, 'fqdn');
$containerName = $name ? "$name ($fqdn)" : $fqdn;
$project = data_get($application, 'environment.project');
$environment = data_get($application, 'environment');
$url = base_url() . '/project/' . $project->uuid . "/" . $environment->name . "/application/" . $application->uuid;
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
}
}
$previews = $application->previews;
foreach ($previews as $preview) {
$foundContainer = $containers->filter(function ($value, $key) use ($id, $uuid, $preview) {
$labels = data_get($value, 'Config.Labels');
$labels = Arr::undot(format_docker_labels_to_json($labels));
$labelId = data_get($labels, 'coolify.applicationId');
if ($labelId == "$id-pr-{$preview->id}") {
return $value;
}
return Str::startsWith(data_get($value, 'Name'), "/$uuid-pr-{$preview->id}");
})->first();
}
}
foreach ($databases as $database) {
$uuid = data_get($database, 'uuid');
$foundContainer = $containers->filter(function ($value, $key) use ($uuid) {
return Str::startsWith(data_get($value, 'Name'), "/$uuid");
})->first();
if ($foundContainer) {
$containerStatus = data_get($foundContainer, 'State.Status');
$databaseStatus = data_get($database, 'status');
if ($containerStatus !== $databaseStatus) {
$database->update(['status' => $containerStatus]);
}
} else {
$databaseStatus = data_get($database, 'status');
if ($databaseStatus !== 'exited') {
$database->update(['status' => 'exited']);
$name = data_get($database, 'name');
$containerName = $name;
$project = data_get($database, 'environment.project');
$environment = data_get($database, 'environment');
$url = base_url() . '/project/' . $project->uuid . "/" . $environment->name . "/database/" . $database->uuid;
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
}
}
}
// TODO Monitor other containers not managed by Coolify
} catch (\Throwable $e) {
send_internal_notification('ContainerStatusJob failed with: ' . $e->getMessage());
ray($e->getMessage());
throw $e;
}
}
}

View File

@@ -4,13 +4,14 @@ namespace App\Jobs;
use App\Actions\CoolifyTask\RunRemoteProcess;
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\SerializesModels;
use Spatie\Activitylog\Models\Activity;
class CoolifyTask implements ShouldQueue
class CoolifyTask implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

View File

@@ -12,6 +12,7 @@ use App\Notifications\Database\BackupFailed;
use App\Notifications\Database\BackupSuccess;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
@@ -20,7 +21,7 @@ use Illuminate\Queue\SerializesModels;
use Throwable;
use Illuminate\Support\Str;
class DatabaseBackupJob implements ShouldQueue
class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

View File

@@ -6,13 +6,14 @@ use App\Models\ApplicationPreview;
use App\Models\StandalonePostgresql;
use App\Notifications\Application\StatusChanged;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class DatabaseContainerStatusJob implements ShouldQueue, ShouldBeUnique
class DatabaseContainerStatusJob implements ShouldQueue, ShouldBeUnique, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

View File

@@ -5,13 +5,14 @@ namespace App\Jobs;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Server;
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 DockerCleanupJob implements ShouldQueue
class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
@@ -36,11 +37,11 @@ class DockerCleanupJob implements ShouldQueue
return;
}
try {
ray()->showQueries()->color('orange');
// ray()->showQueries()->color('orange');
$servers = Server::all();
foreach ($servers as $server) {
if (
!$server->settings->is_reachable && !$server->settings->is_usable
!$server->isFunctional()
) {
continue;
}

View File

@@ -4,13 +4,14 @@ namespace App\Jobs;
use App\Actions\Server\UpdateCoolify;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class InstanceAutoUpdateJob implements ShouldQueue, ShouldBeUnique
class InstanceAutoUpdateJob implements ShouldQueue, ShouldBeUnique, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

View File

@@ -7,6 +7,7 @@ use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
@@ -14,7 +15,7 @@ use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class ProxyContainerStatusJob implements ShouldQueue, ShouldBeUnique
class ProxyContainerStatusJob implements ShouldQueue, ShouldBeUnique, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

View File

@@ -5,6 +5,7 @@ namespace App\Jobs;
use App\Models\InstanceSettings;
use App\Models\Waitlist;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Mail\Message;
@@ -13,7 +14,7 @@ use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;
class SendConfirmationForWaitlistJob implements ShouldQueue
class SendConfirmationForWaitlistJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

View File

@@ -3,13 +3,14 @@
namespace App\Jobs;
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\SerializesModels;
use Illuminate\Support\Facades\Http;
class SendMessageToDiscordJob implements ShouldQueue
class SendMessageToDiscordJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

View File

@@ -3,6 +3,7 @@
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
@@ -10,7 +11,7 @@ use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
use Str;
class SendMessageToTelegramJob implements ShouldQueue
class SendMessageToTelegramJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

View File

@@ -4,13 +4,14 @@ namespace App\Jobs;
use App\Models\Team;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class SubscriptionInvoiceFailedJob implements ShouldQueue
class SubscriptionInvoiceFailedJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Jobs;
use App\Models\Team;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class SubscriptionTrialEndedJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
public Team $team
) {
}
public function handle(): void
{
try {
$session = getStripeCustomerPortalSession($this->team);
$mail = new MailMessage();
$mail->subject('Action required: You trial in Coolify Cloud ended.');
$mail->view('emails.trial-ended', [
'stripeCustomerPortal' => $session->url,
]);
$this->team->members()->each(function ($member) use ($mail) {
if ($member->isAdmin()) {
ray('Sending trial ended email to ' . $member->email);
send_user_an_email($mail, $member->email);
send_internal_notification('Trial reminder email sent to ' . $member->email);
}
});
} catch (\Throwable $e) {
send_internal_notification('SubscriptionTrialEndsSoonJob failed with: ' . $e->getMessage());
ray($e->getMessage());
throw $e;
}
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Jobs;
use App\Models\Team;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class SubscriptionTrialEndsSoonJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
public Team $team
) {
}
public function handle(): void
{
try {
$session = getStripeCustomerPortalSession($this->team);
$mail = new MailMessage();
$mail->subject('You trial in Coolify Cloud ends soon.');
$mail->view('emails.trial-ends-soon', [
'stripeCustomerPortal' => $session->url,
]);
$this->team->members()->each(function ($member) use ($mail) {
if ($member->isAdmin()) {
ray('Sending trial ending email to ' . $member->email);
send_user_an_email($mail, $member->email);
send_internal_notification('Trial reminder email sent to ' . $member->email);
}
});
} catch (\Throwable $e) {
send_internal_notification('SubscriptionTrialEndsSoonJob failed with: ' . $e->getMessage());
ray($e->getMessage());
throw $e;
}
}
}

View File

@@ -105,6 +105,14 @@ class Server extends BaseModel
})->flatten();
}
public function previews() {
return $this->destinations()->map(function ($standaloneDocker) {
return $standaloneDocker->applications->map(function ($application) {
return $application->previews;
})->flatten();
})->flatten();
}
public function destinations()
{
$standalone_docker = $this->hasMany(StandaloneDocker::class)->get();
@@ -153,4 +161,7 @@ class Server extends BaseModel
}
return $shouldRun;
}
public function isFunctional() {
return $this->settings->is_reachable && $this->settings->is_usable;
}
}

View File

@@ -33,7 +33,7 @@ class Subscription extends Model
}
if (isStripe()) {
if (!$this->stripe_plan_id) {
return 'unknown';
return 'zero';
}
$subscription = Subscription::where('id', $this->id)->first();
if (!$subscription) {
@@ -54,6 +54,6 @@ class Subscription extends Model
return Str::of($stripePlanId)->after('stripe_price_id_')->before('_')->lower();
}
}
return 'unknown';
return 'zero';
}
}

View File

@@ -120,4 +120,20 @@ class Team extends Model implements SendsDiscord, SendsEmail
{
return $this->hasMany(S3Storage::class);
}
public function trialEnded() {
foreach ($this->servers as $server) {
$server->settings()->update([
'is_usable' => false,
'is_reachable' => false,
]);
}
}
public function trialEndedButSubscribed() {
foreach ($this->servers as $server) {
$server->settings()->update([
'is_usable' => true,
'is_reachable' => true,
]);
}
}
}

View File

@@ -53,7 +53,7 @@ class DeploymentSuccess extends Notification implements ShouldQueue
$pull_request_id = data_get($this->preview, 'pull_request_id', 0);
$fqdn = $this->fqdn;
if ($pull_request_id === 0) {
$mail->subject("✅New version is deployed of {$this->application_name}");
$mail->subject(" New version is deployed of {$this->application_name}");
} else {
$fqdn = $this->preview->fqdn;
$mail->subject("✅ Pull request #{$pull_request_id} of {$this->application_name} deployed successfully");

View File

@@ -0,0 +1,62 @@
<?php
namespace App\Notifications\Container;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class ContainerRestarted extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 5;
public function __construct(public string $name, public Server $server, public ?string $url = null)
{
}
public function via(object $notifiable): array
{
return setNotificationChannels($notifiable, 'status_changes');
}
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject("✅ Container ({$this->name}) has been restarted automatically on {$this->server->name}");
$mail->view('emails.container-restarted', [
'containerName' => $this->name,
'serverName' => $this->server->name,
'url' => $this->url ,
]);
return $mail;
}
public function toDiscord(): string
{
$message = "✅ Container ({$this->name}) has been restarted automatically on {$this->server->name}";
return $message;
}
public function toTelegram(): array
{
$message = "✅ Container ({$this->name}) has been restarted automatically on {$this->server->name}";
$payload = [
"message" => $message,
];
if ($this->url) {
$payload['buttons'] = [
[
[
"text" => "Check Proxy in Coolify",
"url" => $this->url
]
]
];
};
return $payload;
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace App\Notifications\Container;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class ContainerStopped extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 1;
public function __construct(public string $name, public Server $server, public ?string $url = null)
{
}
public function via(object $notifiable): array
{
return setNotificationChannels($notifiable, 'status_changes');
}
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject("⛔ Container {$this->name} has been stopped on {$this->server->name}");
$mail->view('emails.container-stopped', [
'containerName' => $this->name,
'serverName' => $this->server->name,
'url' => $this->url,
]);
return $mail;
}
public function toDiscord(): string
{
$message = "⛔ Container {$this->name} has been stopped on {$this->server->name}";
return $message;
}
public function toTelegram(): array
{
$message = "⛔ Container ($this->name} has been stopped on {$this->server->name}";
$payload = [
"message" => $message,
];
if ($this->url) {
$payload['buttons'] = [
[
[
"text" => "Open Application in Coolify",
"url" => $this->url
]
]
];
}
return $payload;
}
}

View File

@@ -1,53 +0,0 @@
<?php
namespace App\Notifications\Server;
use App\Models\Server;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Str;
class NotReachable extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 5;
public function __construct(public Server $server)
{
}
public function via(object $notifiable): array
{
return setNotificationChannels($notifiable, 'status_changes');
}
public function toMail(): MailMessage
{
$mail = new MailMessage();
// $fqdn = $this->fqdn;
$mail->subject("⛔ Server '{$this->server->name}' is unreachable");
// $mail->view('emails.application-status-changes', [
// 'name' => $this->application_name,
// 'fqdn' => $fqdn,
// 'application_url' => $this->application_url,
// ]);
return $mail;
}
public function toDiscord(): string
{
$message = '⛔ Server \'' . $this->server->name . '\' is unreachable (could be a temporary issue). If you receive this more than twice in a row, please check your server.';
return $message;
}
public function toTelegram(): array
{
return [
"message" => '⛔ Server \'' . $this->server->name . '\' is unreachable (could be a temporary issue). If you receive this more than twice in a row, please check your server.'
];
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace App\Notifications\Server;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class Unreachable extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 1;
public function __construct(public Server $server)
{
}
public function via(object $notifiable): array
{
return setNotificationChannels($notifiable, 'status_changes');
}
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject("⛔ Server ({$this->server->name}) is unreachable after trying to connect to it 5 times");
$mail->view('emails.server-lost-connection', [
'name' => $this->server->name,
]);
return $mail;
}
public function toDiscord(): string
{
$message = "⛔ Server '{$this->server->name}' is unreachable after trying to connect to it 5 times. All automations & integrations are turned off! Please check your server! IMPORTANT: You have to validate your server again after you fix the issue.";
return $message;
}
public function toTelegram(): array
{
return [
"message" => "⛔ Server '{$this->server->name}' is unreachable after trying to connect to it 5 times. All automations & integrations are turned off! Please check your server! IMPORTANT: You have to validate your server again after you fix the issue."
];
}
}

View File

@@ -28,9 +28,8 @@ trait ExecuteRemoteCommand
$ip = data_get($this->server, 'ip');
$user = data_get($this->server, 'user');
$port = data_get($this->server, 'port');
$private_key_location = get_private_key_for_server($this->server);
$commandsText->each(function ($single_command) use ($private_key_location, $ip, $user, $port) {
$commandsText->each(function ($single_command) use ($ip, $user, $port) {
$command = data_get($single_command, 'command') ?? $single_command[0] ?? null;
if ($command === null) {
throw new \RuntimeException('Command is not set');
@@ -39,8 +38,8 @@ trait ExecuteRemoteCommand
$ignore_errors = data_get($single_command, 'ignore_errors', false);
$this->save = data_get($single_command, 'save');
$remote_command = generate_ssh_command($private_key_location, $ip, $user, $port, $command);
$process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden) {
$remote_command = generateSshCommand( $ip, $user, $port, $command);
$process = processWithEnv()->timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden) {
$output = Str::of($output)->trim();
$new_log_entry = [
'command' => $command,

View File

@@ -26,8 +26,11 @@ function format_docker_command_output_to_json($rawOutput): Collection
->map(fn ($outputLine) => json_decode($outputLine, true, flags: JSON_THROW_ON_ERROR));
}
function format_docker_labels_to_json($rawOutput): Collection
function format_docker_labels_to_json(string|Array $rawOutput): Collection
{
if (is_array($rawOutput)) {
return collect($rawOutput);
}
$outputLines = explode(PHP_EOL, $rawOutput);
return collect($outputLines)
@@ -88,7 +91,7 @@ function generateApplicationContainerName(string $uuid, int $pull_request_id = 0
{
$now = now()->format('Hisu');
if ($pull_request_id !== 0 && $pull_request_id !== null) {
return $uuid . '-pr-' . $pull_request_id . '-' . $now;
return $uuid . '-pr-' . $pull_request_id;
} else {
return $uuid . '-' . $now;
}

View File

@@ -33,12 +33,9 @@ function remote_process(
}
}
$private_key_location = save_private_key_for_server($server);
return resolve(PrepareCoolifyTask::class, [
'remoteProcessArgs' => new CoolifyTaskArgs(
server_ip: $server->ip,
private_key_location: $private_key_location,
command: <<<EOT
{$command_string}
EOT,
@@ -52,37 +49,44 @@ function remote_process(
])();
}
function get_private_key_for_server(Server $server)
{
$temp_file = "id.root@{$server->ip}";
return '/var/www/html/storage/app/ssh/keys/' . $temp_file;
}
function save_private_key_for_server(Server $server)
function removePrivateKeyFromSshAgent(Server $server)
{
if (data_get($server, 'privateKey.private_key') === null) {
throw new \Exception("Server {$server->name} does not have a private key");
}
$temp_file = "id.root@{$server->ip}";
Storage::disk('ssh-keys')->put($temp_file, $server->privateKey->private_key);
Storage::disk('ssh-mux')->makeDirectory('.');
return '/var/www/html/storage/app/ssh/keys/' . $temp_file;
processWithEnv()->run("echo '{$server->privateKey->private_key}' | ssh-add -d -");
}
function addPrivateKeyToSshAgent(Server $server)
{
if (data_get($server, 'privateKey.private_key') === null) {
throw new \Exception("Server {$server->name} does not have a private key");
}
// ray('adding key', $server->privateKey->private_key);
processWithEnv()->run("echo '{$server->privateKey->private_key}' | ssh-add -q -");
}
function generate_ssh_command(string $private_key_location, string $server_ip, string $user, string $port, string $command, bool $isMux = true)
function generateSshCommand(string $server_ip, string $user, string $port, string $command, bool $isMux = true)
{
$server = Server::where('ip', $server_ip)->first();
if (!$server) {
throw new \Exception("Server with ip {$server_ip} not found");
}
addPrivateKeyToSshAgent($server);
$timeout = config('constants.ssh.command_timeout');
$connectionTimeout = config('constants.ssh.connection_timeout');
$serverInterval = config('constants.ssh.server_interval');
$delimiter = 'EOF-COOLIFY-SSH';
$ssh_command = "ssh ";
$ssh_command = "timeout $timeout ssh ";
if ($isMux && config('coolify.mux_enabled')) {
$ssh_command .= '-o ControlMaster=auto -o ControlPersist=1m -o ControlPath=/var/www/html/storage/app/ssh/mux/%h_%p_%r ';
}
$command = "PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin && $command";
$ssh_command .= "-i {$private_key_location} "
. '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null '
$ssh_command .= '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null '
. '-o PasswordAuthentication=no '
. '-o ConnectTimeout=3600 '
. '-o ServerAliveInterval=20 '
. "-o ConnectTimeout=$connectionTimeout "
. "-o ServerAliveInterval=$serverInterval "
. '-o RequestTTY=no '
. '-o LogLevel=ERROR '
. "-p {$port} "
@@ -90,11 +94,16 @@ function generate_ssh_command(string $private_key_location, string $server_ip, s
. " 'bash -se' << \\$delimiter" . PHP_EOL
. $command . PHP_EOL
. $delimiter;
// ray($ssh_command);
return $ssh_command;
}
function instantCommand(string $command, $throwError = true) {
$process = Process::run($command);
function processWithEnv()
{
return Process::env(['SSH_AUTH_SOCK' => config('coolify.ssh_auth_sock')]);
}
function instantCommand(string $command, $throwError = true)
{
$process = processWithEnv()->run($command);
$output = trim($process->output());
$exitCode = $process->exitCode();
if ($exitCode !== 0) {
@@ -108,9 +117,8 @@ function instantCommand(string $command, $throwError = true) {
function instant_remote_process(array $command, Server $server, $throwError = true, $repeat = 1)
{
$command_string = implode("\n", $command);
$private_key_location = save_private_key_for_server($server);
$ssh_command = generate_ssh_command($private_key_location, $server->ip, $server->user, $server->port, $command_string);
$process = Process::run($ssh_command);
$ssh_command = generateSshCommand($server->ip, $server->user, $server->port, $command_string);
$process = processWithEnv()->run($ssh_command);
$output = trim($process->output());
$exitCode = $process->exitCode();
if ($exitCode !== 0) {
@@ -168,6 +176,7 @@ function refresh_server_connection(PrivateKey $private_key)
// currentTeam()->privateKeys = PrivateKey::where('team_id', currentTeam()->id)->get();
// }
}
removePrivateKeyFromSshAgent($server);
}
function validateServer(Server $server)
@@ -208,7 +217,7 @@ function validateServer(Server $server)
$server->settings->is_usable = false;
throw $e;
} finally {
if(data_get($server,'settings')) $server->settings->save();
if (data_get($server, 'settings')) $server->settings->save();
}
}

View File

@@ -262,21 +262,33 @@ function send_internal_notification(string $message): void
ray($e->getMessage());
}
}
function send_user_an_email(MailMessage $mail, string $email): void
function send_user_an_email(MailMessage $mail, string $email, ?string $cc = null): void
{
$settings = InstanceSettings::get();
$type = set_transanctional_email_settings($settings);
if (!$type) {
throw new Exception('No email settings found.');
}
Mail::send(
[],
[],
fn (Message $message) => $message
->to($email)
->subject($mail->subject)
->html((string) $mail->render())
);
if ($cc) {
Mail::send(
[],
[],
fn (Message $message) => $message
->to($email)
->cc($cc)
->subject($mail->subject)
->html((string) $mail->render())
);
} else {
Mail::send(
[],
[],
fn (Message $message) => $message
->to($email)
->subject($mail->subject)
->html((string) $mail->render())
);
}
}
function isEmailEnabled($notifiable)
{

View File

@@ -1,5 +1,10 @@
<?php
return [
'ssh' =>[
'connection_timeout' => 10,
'server_interval' => 20,
'command_timeout' => 7200,
],
'waitlist' => [
'expiration' => 10,
],
@@ -10,6 +15,7 @@ return [
],
],
'limits' => [
'trial_period'=> 14,
'server' => [
'zero' => 0,
'self-hosted' => 999999999999,

View File

@@ -8,4 +8,5 @@ return [
'dev_webhook' => env('SERVEO_URL'),
'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),
'helper_image' => env('HELPER_IMAGE', 'ghcr.io/coollabsio/coolify-helper:latest'),
'ssh_auth_sock' => env('SSH_AUTH_SOCK', '/tmp/coolify-ssh-agent.sock'),
];

View File

@@ -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.34',
'release' => '4.0.0-beta.36',
'server_name' => env('APP_ID', 'coolify'),
// When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'),

View File

@@ -1,3 +1,3 @@
<?php
return '4.0.0-beta.34';
return '4.0.0-beta.36';

View File

@@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->dropColumn('nixpkgsarchive');
});
}
public function down(): void
{
Schema::table('teams', function (Blueprint $table) {
$table->string('nixpkgsarchive')->nullable();
});
}
};

View File

@@ -0,0 +1,30 @@
<?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('subscriptions', function (Blueprint $table) {
$table->boolean('stripe_trial_already_ended')->default(false)->after('stripe_cancel_at_period_end');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('subscriptions', function (Blueprint $table) {
$table->dropColumn('stripe_trial_already_ended');
});
}
};

View File

@@ -21,6 +21,7 @@ services:
SSL_MODE: "off"
AUTORUN_LARAVEL_STORAGE_LINK: "false"
AUTORUN_LARAVEL_MIGRATION: "false"
SSH_AUTH_SOCK: "/tmp/coolify-ssh-agent.sock"
volumes:
- .:/var/www/html/:cached
postgres:

View File

@@ -64,6 +64,7 @@ services:
- LEMON_SQUEEZY_BASIC_PLAN_IDS
- LEMON_SQUEEZY_PRO_PLAN_IDS
- LEMON_SQUEEZY_ULTIMATE_PLAN_IDS
- SSH_AUTH_SOCK="/tmp/coolify-ssh-agent.sock"
ports:
- "${APP_PORT:-8000}:80"
expose:

View File

@@ -24,4 +24,4 @@ RUN echo "alias mfs='php artisan migrate:fresh --seed'" >>/etc/bash.bashrc
RUN echo "alias cda='composer dump-autoload'" >>/etc/bash.bashrc
RUN echo "alias run='./scripts/run'" >>/etc/bash.bashrc
# COPY --chmod=755 docker/dev-ssu/etc/s6-overlay/ /etc/s6-overlay/
COPY --chmod=755 docker/dev-ssu/etc/s6-overlay/ /etc/s6-overlay/

View File

@@ -0,0 +1,5 @@
#!/command/execlineb -P
foreground {
s6-sleep 5
su - webuser -c "php /var/www/html/artisan horizon"
}

View File

@@ -1,2 +0,0 @@
#!/command/execlineb -P
su - webuser -c "php /var/www/html/artisan queue:listen"

View File

@@ -1,2 +1,5 @@
#!/command/execlineb -P
su - webuser -c "php /var/www/html/artisan schedule:work"
foreground {
s6-sleep 5
su - webuser -c "php /var/www/html/artisan schedule:work"
}

View File

@@ -0,0 +1 @@
oneshot

View File

@@ -0,0 +1,5 @@
#!/usr/bin/execlineb -P
foreground {
s6-sleep 5
su - webuser -c "ssh-agent -a /tmp/coolify-ssh-agent.sock"
}

View File

@@ -0,0 +1 @@
oneshot

View File

@@ -0,0 +1,5 @@
#!/usr/bin/execlineb -P
foreground {
s6-sleep 5
su - webuser -c "ssh-agent -a /tmp/coolify-ssh-agent.sock"
}

View File

@@ -15,7 +15,7 @@
@if (is_transactional_emails_active())
<form action="/forgot-password" method="POST" class="flex flex-col gap-2">
@csrf
<x-forms.input required value="test@example.com" type="email" name="email"
<x-forms.input required type="email" name="email"
label="{{ __('input.email') }}" autofocus />
<x-forms.button type="submit">{{ __('auth.forgot_password_send_email') }}</x-forms.button>
</form>

View File

@@ -23,7 +23,7 @@
<div>
<form action="/login" method="POST" class="flex flex-col gap-2">
@csrf
@env('local')
@env('local')
<x-forms.input value="test@example.com" type="email" name="email"
label="{{ __('input.email') }}" autofocus />
@@ -35,8 +35,10 @@
@else
<x-forms.input type="email" name="email" label="{{ __('input.email') }}" autofocus />
<x-forms.input type="password" name="password" label="{{ __('input.password') }}" />
<a href="/forgot-password" class="text-xs">
{{ __('auth.forgot_password') }}?
</a>
@endenv
<x-forms.button type="submit">{{ __('auth.login') }}</x-forms.button>
@if (!$is_registration_enabled)
<div class="text-center ">{{ __('auth.registration_disabled') }}</div>

View File

@@ -14,12 +14,12 @@
</div>
@endif
</div>
@if($explanation)
@isset($explanation)
<div class="col-span-1">
<h1 class="pb-8 font-bold">Explanation</h1>
<div class="space-y-4">
{{$explanation}}
</div>
</div>
@endif
@endisset
</div>

View File

@@ -3,4 +3,4 @@
Thank you,<br>
{{ config('app.name') ?? 'Coolify' }}
{{ Illuminate\Mail\Markdown::parse('[Contact Support](https://docs.coollabs.io)') }}
{{ Illuminate\Mail\Markdown::parse('[Contact Support](https://docs.coollabs.io/contact)') }}

View File

@@ -10,6 +10,18 @@
</svg>
</a>
</li>
<li title="Help" class="mt-auto">
<div class="justify-center icons" wire:click="help" onclick="help.showModal()">
<svg class="{{ request()->is('help*') ? 'text-warning icon' : 'icon' }}" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0-18 0m9 4v.01" />
<path d="M12 13a2 2 0 0 0 .914-3.782a1.98 1.98 0 0 0-2.414.483" />
</g>
</svg>
</div>
</li>
<li class="pb-6" title="Logout">
<form action="/logout" method="POST" class=" hover:bg-transparent">
@csrf

View File

@@ -21,6 +21,7 @@
</label>
</fieldset>
</div>
<div class="py-2 text-center"><span class="font-bold text-warning">{{config('constants.limits.trial_period')}} days trial</span> included on all plans, without credit card details.</div>
<div x-show="selected === 'monthly'" class="flex justify-center h-10 mt-3 text-sm leading-6 ">
<div>Save <span class="font-bold text-warning">1 month</span> annually with the yearly plans.
</div>

View File

@@ -0,0 +1,8 @@
<x-emails.layout>
We would like to inform you that a {{config('constants.limits.trial_period')}} days of trial has been added to all subscription plans.
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.
</x-emails.layout>

View File

@@ -0,0 +1,15 @@
<x-emails.layout>
Container ({{ $containerName }}) has been restarted automatically on {{$serverName}}, because it was stopped unexpected.
@if ($containerName === 'coolify-proxy')
Coolify Proxy should run on your server as you have FQDNs set up in one of your resources.
Note: The proxy should not stop unexpectedly, so please check what is going on your server.
If you don't want to use Coolify Proxy, please remove FQDN from your resources or set Proxy type to Custom(None).
@endif
</x-emails.layout>

View File

@@ -0,0 +1,9 @@
<x-emails.layout>
Container {{ $containerName }} has been stopped unexpected on {{$serverName}}.
@if ($url)
Please check what is going on [here]({{ $url }}).
@endif
</x-emails.layout>

View File

@@ -0,0 +1,12 @@
<x-emails.layout>
Coolify cannot connect to your server ({{$name}}). Please check your server and make sure it is running.
All automations & integrations are turned off!
IMPORTANT: You have to validate your server again after you fix the issue.
If you have any questions, please contact us.
</x-emails.layout>

View File

@@ -0,0 +1,7 @@
<x-emails.layout>
Your trial ended. All automations and integrations are disabled for all of your servers.
Please update payment details [here]({{$stripeCustomerPortal}}) or in [Coolify Cloud](https://app.coolify.io) to continue using our services.
</x-emails.layout>

View File

@@ -0,0 +1,7 @@
<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.
</x-emails.layout>

View File

@@ -28,14 +28,12 @@
<body>
@livewireScripts
@if (isSubscriptionActive() || isDev())
<dialog id="help" class="modal">
<livewire:help />
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
@endif
<dialog id="help" class="modal">
<livewire:help />
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
<x-toaster-hub />
<x-version class="fixed left-2 bottom-1" />
<script>

View File

@@ -5,12 +5,34 @@
<h1 class="text-5xl font-bold">Welcome to Coolify</h1>
<p class="py-6 text-xl text-center">Let me help you to set the basics.</p>
<div class="flex justify-center ">
<x-forms.button class="justify-center box" wire:click="welcome">Get Started
<x-forms.button class="justify-center box" wire:click="$set('currentState','explanation')">Get Started
</x-forms.button>
</div>
@endif
</div>
<div>
@if ($currentState === 'explanation')
<x-boarding-step title="What is Coolify?">
<x-slot:question>
Coolify is an all-in-one application to automate tasks on your servers, deploy application with Git
integrations, deploy databases and services, monitor these resources with notifications and alerts
without vendor lock-in
and <a href="https://coolify.io" class="text-white hover:underline">much much more</a>.
<br><br>
<span class="text-xl">
<x-highlighted text="Self-hosting with superpowers!" /></span>
</x-slot:question>
<x-slot:explanation>
<p><x-highlighted text="Task automation:" /> You do not to manage your servers too much. Coolify do it for you.</p>
<p><x-highlighted text="No vendor lock-in:" /> All configurations are stored on your server, so everything works without Coolify (except integrations and automations).</p>
<p><x-highlighted text="Monitoring:" />You will get notified on your favourite platform (Discord, Telegram, Email, etc.) when something goes wrong, or an action needed from your side.</p>
</x-slot:explanation>
<x-slot:actions>
<x-forms.button class="justify-center box" wire:click="explanation">Next
</x-forms.button>
</x-slot:actions>
</x-boarding-step>
@endif
@if ($currentState === 'select-server-type')
<x-boarding-step title="Server">
<x-slot:question>
@@ -18,9 +40,11 @@
or on a <x-highlighted text="Remote Server" />?
</x-slot:question>
<x-slot:actions>
<x-forms.button class="justify-center box" wire:target="setServerType('localhost')" wire:click="setServerType('localhost')">Localhost
<x-forms.button class="justify-center box" wire:target="setServerType('localhost')"
wire:click="setServerType('localhost')">Localhost
</x-forms.button>
<x-forms.button class="justify-center box" wire:target="setServerType('remote')" wire:click="setServerType('remote')">Remote Server
<x-forms.button class="justify-center box" wire:target="setServerType('remote')"
wire:click="setServerType('remote')">Remote Server
</x-forms.button>
</x-slot:actions>
<x-slot:explanation>
@@ -42,9 +66,11 @@
Do you have your own SSH Private Key?
</x-slot:question>
<x-slot:actions>
<x-forms.button class="justify-center box" wire:target="setPrivateKey('own')" wire:click="setPrivateKey('own')">Yes
<x-forms.button class="justify-center box" wire:target="setPrivateKey('own')"
wire:click="setPrivateKey('own')">Yes
</x-forms.button>
<x-forms.button class="justify-center box" wire:target="setPrivateKey('create')" wire:click="setPrivateKey('create')">No (create one for me)
<x-forms.button class="justify-center box" wire:target="setPrivateKey('create')"
wire:click="setPrivateKey('create')">No (create one for me)
</x-forms.button>
@if (count($privateKeys) > 0)
<form wire:submit.prevent='selectExistingPrivateKey' class="flex flex-col w-full gap-4 pr-10">
@@ -115,9 +141,10 @@
<x-forms.textarea required placeholder="-----BEGIN OPENSSH PRIVATE KEY-----" label="Private Key"
id="privateKey" />
@if ($privateKeyType === 'create')
<x-forms.textarea rows="7" readonly label="Public Key" id="publicKey" />
<span class="font-bold text-warning">ACTION REQUIRED: Copy the 'Public Key' to your server's ~/.ssh/authorized_keys
file.</span>
<x-forms.textarea rows="7" readonly label="Public Key" id="publicKey" />
<span class="font-bold text-warning">ACTION REQUIRED: Copy the 'Public Key' to your server's
~/.ssh/authorized_keys
file.</span>
@endif
<x-forms.button type="submit">Save</x-forms.button>
</form>
@@ -182,7 +209,8 @@
Could not find Docker Engine on your server. Do you want me to install it for you?
</x-slot:question>
<x-slot:actions>
<x-forms.button class="justify-center box" wire:click="installDocker" onclick="installDocker.showModal()">
<x-forms.button class="justify-center box" wire:click="installDocker"
onclick="installDocker.showModal()">
Let's do
it!</x-forms.button>
</x-slot:actions>
@@ -233,12 +261,14 @@
@endif
</x-slot:question>
<x-slot:actions>
<x-forms.button class="justify-center box" wire:click="createNewProject">Let's create a new one!</x-forms.button>
<x-forms.button class="justify-center box" wire:click="createNewProject">Let's create a new
one!</x-forms.button>
<div>
@if (count($projects) > 0)
<form wire:submit.prevent='selectExistingProject'
class="flex flex-col w-full gap-4 lg:w-96">
<x-forms.select label="Existing projects" class="w-96" id='selectedExistingProject'>
<x-forms.select label="Existing projects" class="w-96"
id='selectedExistingProject'>
@foreach ($projects as $project)
<option wire:key="{{ $loop->index }}" value="{{ $project->id }}">
{{ $project->name }}</option>

View File

@@ -1,5 +1,5 @@
<div>
@if ($server->settings->is_usable)
@if ($server->isFunctional())
<div class="flex items-end gap-2">
<h2>Destinations</h2>
<a href="{{ route('destination.new', ['server_id' => $server->id]) }}">

View File

@@ -32,10 +32,6 @@
<option value="dockerfile">Dockerfile</option>
<option disabled value="compose">Compose</option>
</x-forms.select>
{{-- @if ($application->build_pack === 'nixpacks')
<x-forms.input id="application.nixpkgsarchive" label="NixPackages Archive (nixpkgsArchive)"
helper="You can customize the NixPackages archive to use."> </x-forms.input>
@endif --}}
</div>
@if ($application->settings->is_static)
<x-forms.select id="application.static_image" label="Static Image" required>

View File

@@ -48,10 +48,10 @@
@endif
</div>
@if ($application->previews->count() > 0)
<h4 class="py-4" wire:poll.10000ms='previewRefresh'>Deployed Previews</h4>
<h4 class="py-4">Deployed Previews</h4>
<div class="flex gap-6 ">
@foreach ($application->previews as $preview)
<div class="flex flex-col p-4 bg-coolgray-200 " x-init="$wire.loadStatus('{{ data_get($preview, 'pull_request_id') }}')">
<div class="flex flex-col p-4 bg-coolgray-200">
<div class="flex gap-2">PR #{{ data_get($preview, 'pull_request_id') }} |
@if (data_get($preview, 'status') === 'running')
<x-status.running />

View File

@@ -20,7 +20,7 @@
@endif
</div>
@if (!$server->settings->is_reachable || !$server->settings->is_usable)
@if (!$server->isFunctional())
You can't use this server until it is validated.
@else
Server validated.
@@ -35,11 +35,7 @@
label="Is it part of a Swarm cluster?" /> --}}
</div>
<div class="flex flex-col w-full gap-2 lg:flex-row">
@if ($server->id === 0)
<x-forms.input id="server.ip" label="IP Address" required />
@else
<x-forms.input id="server.ip" label="IP Address" readonly required />
@endif
<x-forms.input id="server.ip" label="IP Address" required />
<div class="flex gap-2">
<x-forms.input id="server.user" label="User" required />
<x-forms.input type="number" id="server.port" label="Port" required />
@@ -52,15 +48,15 @@
</x-forms.button>
@endif
@if ($server->settings->is_reachable && !$server->settings->is_usable && $server->id !== 0)
<x-forms.button wire:poll.2000ms='validateServer' class="mt-8 mb-4 box" onclick="installDocker.showModal()"
wire:click.prevent='installDocker' isHighlighted>
<x-forms.button class="mt-8 mb-4 box" onclick="installDocker.showModal()" wire:click.prevent='installDocker'
isHighlighted>
Install Docker Engine 24.0
</x-forms.button>
@endif
@if ($server->settings->is_usable)
@if ($server->isFunctional())
<h3 class="py-4">Settings</h3>
<x-forms.input id="cleanup_after_percentage" label="Disk Cleanup threshold (%)" required
helper="Disk cleanup job will be executed if disk usage is more than this number." />
<x-forms.input id="cleanup_after_percentage" label="Disk Cleanup threshold (%)" required
helper="Disk cleanup job will be executed if disk usage is more than this number." />
@endif
</form>
<h2 class="pt-4">Danger Zone</h2>

View File

@@ -1,5 +1,5 @@
<div>
@if (data_get($server,'settings.is_usable'))
@if ($server->isFunctional())
@if (data_get($server,'proxy.type'))
<x-modal submitWireAction="proxyStatusUpdated" modalId="startProxy">
<x-slot:modalBody>

View File

@@ -11,7 +11,7 @@
@if (data_get($server, 'proxy.status') === 'running')
<div class="flex gap-4">
<button>
<a target="_blank" href="{{ base_url(false) }}:8080">
<a target="_blank" href="http://{{$server->ip}}:8080">
Traefik Dashboard
<x-external-link />
</a>

View File

@@ -2,29 +2,29 @@
@if (config('subscription.provider') === 'stripe')
<x-slot:basic>
<x-forms.button x-show="selected === 'monthly'" x-cloak aria-describedby="tier-basic"
class="w-full h-10 buyme" wire:click="subscribeStripe('basic-monthly')"> Subscribe
class="w-full h-10 buyme" wire:click="subscribeStripe('basic-monthly')"> {{$isTrial ? 'Start Trial' : 'Subscribe' }}
</x-forms.button>
<x-forms.button x-show="selected === 'yearly'" x-cloak aria-describedby="tier-basic"
class="w-full h-10 buyme" wire:click="subscribeStripe('basic-yearly')"> Subscribe
class="w-full h-10 buyme" wire:click="subscribeStripe('basic-yearly')"> {{$isTrial ? 'Start Trial' : 'Subscribe' }}
</x-forms.button>
</x-slot:basic>
<x-slot:pro>
<x-forms.button x-show="selected === 'monthly'" x-cloak aria-describedby="tier-pro"
class="w-full h-10 buyme" wire:click="subscribeStripe('pro-monthly')"> Subscribe
class="w-full h-10 buyme" wire:click="subscribeStripe('pro-monthly')"> {{$isTrial ? 'Start Trial' : 'Subscribe' }}
</x-forms.button>
<x-forms.button x-show="selected === 'yearly'" x-cloak aria-describedby="tier-pro" class="w-full h-10 buyme"
wire:click="subscribeStripe('pro-yearly')"> Subscribe
wire:click="subscribeStripe('pro-yearly')"> {{$isTrial ? 'Start Trial' : 'Subscribe' }}
</x-forms.button>
</x-slot:pro>
<x-slot:ultimate>
<x-forms.button x-show="selected === 'monthly'" x-cloak aria-describedby="tier-ultimate"
class="w-full h-10 buyme" wire:click="subscribeStripe('ultimate-monthly')"> Subscribe
class="w-full h-10 buyme" wire:click="subscribeStripe('ultimate-monthly')"> {{$isTrial ? 'Start Trial' : 'Subscribe' }}
</x-forms.button>
<x-forms.button x-show="selected === 'yearly'" x-cloak aria-describedby="tier-ultimate"
class="w-full h-10 buyme" wire:click="subscribeStripe('ultimate-yearly')"> Subscribe
class="w-full h-10 buyme" wire:click="subscribeStripe('ultimate-yearly')"> {{$isTrial ? 'Start Trial' : 'Subscribe' }}
</x-forms.button>
</x-slot:ultimate>
@endif

View File

@@ -30,6 +30,10 @@ use Laravel\Fortify\Fortify;
Route::post('/forgot-password', function (Request $request) {
if (is_transactional_emails_active()) {
$arrayOfRequest = $request->only(Fortify::email());
$request->merge([
'email' => Str::lower($arrayOfRequest['email']),
]);
$type = set_transanctional_email_settings();
if (!$type) {
return response()->json(['message' => 'Transactional emails are not active'], 400);

View File

@@ -1,6 +1,7 @@
<?php
use App\Jobs\SubscriptionInvoiceFailedJob;
use App\Jobs\SubscriptionTrialEndedJob;
use App\Jobs\SubscriptionTrialEndsSoonJob;
use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Models\GithubApp;
@@ -115,9 +116,14 @@ Route::post('/source/github/events', function () {
$applications = $applications->where('git_branch', $base_branch)->get();
}
if ($applications->isEmpty()) {
return response('Nothing to do. No applications found.');
return response("Nothing to do. No applications found with branch '$base_branch'.");
}
foreach ($applications as $application) {
$isFunctional = $application->destination->server->isFunctional();
if (!$isFunctional) {
ray('Server is not functional: ' . $application->destination->server->name);
continue;
}
if ($x_github_event === 'push') {
if ($application->isDeployable()) {
ray('Deploying ' . $application->name . ' with branch ' . $branch);
@@ -172,6 +178,7 @@ Route::post('/source/github/events', function () {
}
}
} catch (Exception $e) {
ray($e->getMessage());
return general_error_handler(err: $e);
}
});
@@ -271,20 +278,17 @@ Route::post('/payments/stripe/events', function () {
case 'invoice.paid':
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
$planId = data_get($data, 'lines.data.0.plan.id');
$subscription->update([
'stripe_plan_id' => $planId,
'stripe_invoice_paid' => true,
]);
break;
// case 'invoice.payment_failed':
// $customerId = data_get($data, 'customer');
// $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
// if ($subscription) {
// SubscriptionInvoiceFailedJob::dispatch($subscription->team);
// }
// break;
case 'customer.subscription.updated':
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
$trialEndedAlready = data_get($subscription, 'stripe_trial_already_ended');
$status = data_get($data, 'status');
$subscriptionId = data_get($data, 'items.data.0.subscription');
$planId = data_get($data, 'items.data.0.plan.id');
$cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end');
@@ -297,7 +301,19 @@ Route::post('/payments/stripe/events', function () {
'stripe_plan_id' => $planId,
'stripe_cancel_at_period_end' => $cancelAtPeriodEnd,
]);
ray($feedback, $comment, $alreadyCancelAtPeriodEnd, $cancelAtPeriodEnd);
if ($status === 'paused') {
$subscription->update([
'stripe_invoice_paid' => false,
]);
send_internal_notification('Subscription paused for team: ' . $subscription->team->id);
}
// Trial ended but subscribed, reactive servers
if ($trialEndedAlready && $status === 'active') {
$team = data_get($subscription, 'team');
$team->trialEndedButSubscribed();
}
if ($feedback) {
$reason = "Cancellation feedback for {$subscription->team->id}: '" . $feedback . "'";
if ($comment) {
@@ -305,7 +321,6 @@ Route::post('/payments/stripe/events', function () {
}
send_internal_notification($reason);
}
ray($alreadyCancelAtPeriodEnd !== $cancelAtPeriodEnd);
if ($alreadyCancelAtPeriodEnd !== $cancelAtPeriodEnd) {
if ($cancelAtPeriodEnd) {
send_internal_notification('Subscription cancelled at period end for team: ' . $subscription->team->id);
@@ -315,16 +330,44 @@ Route::post('/payments/stripe/events', function () {
}
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('Subscription cancelled: ' . $subscription->team->id);
break;
case 'customer.subscription.trial_will_end':
$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 team: ' . $subscription->team->id);
break;
default:
// Unhandled event type
}

View File

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