Compare commits

...

95 Commits

Author SHA1 Message Date
Andras Bacsai
5682ab9570 Merge pull request #2261 from coollabsio/next
v4.0.0-beta.285
2024-05-21 17:47:04 +02:00
Andras Bacsai
a3d73634e7 feat: scheduled task failed notification 2024-05-21 15:36:26 +02:00
Andras Bacsai
98b6aec203 feat: admin view for deleting users 2024-05-21 14:29:06 +02:00
Andras Bacsai
7feb788ed3 fix: show it docker compose has syntax errors 2024-05-21 12:02:04 +02:00
Andras Bacsai
bea490081b ui: responsive here and there 2024-05-21 11:23:53 +02:00
Andras Bacsai
7adc3ca003 feat: Add SerpAPI as a Github Sponsor 2024-05-21 11:20:12 +02:00
Andras Bacsai
f8cbc63ab0 fix: optimize new resource creation 2024-05-21 10:17:32 +02:00
Andras Bacsai
418590fb35 Update version numbers to 4.0.0-beta.285 2024-05-21 10:16:31 +02:00
Andras Bacsai
56144482f1 Merge pull request #2244 from coollabsio/next
v4.0.0-beta.284
2024-05-19 20:56:17 +02:00
Andras Bacsai
59f681e6af revert: hc return code check 2024-05-19 20:54:16 +02:00
Andras Bacsai
d3296f5180 feat: add hc logs to healthchecks 2024-05-18 18:48:33 +02:00
Andras Bacsai
c6fff0aa13 Update version numbers to 4.0.0-beta.284 2024-05-17 18:54:21 +02:00
Andras Bacsai
41e0c42282 Remove warning message about only supporting root user login via SSH in the future 2024-05-17 18:54:11 +02:00
Andras Bacsai
ede2274816 Merge pull request #2234 from coollabsio/next
v4.0.0-beta.283
2024-05-17 15:34:00 +02:00
Andras Bacsai
ead672afb2 fix: PR deployments have good predefined envs 2024-05-17 15:30:27 +02:00
Andras Bacsai
73bc7b045e feat: Add pull_request_id filter to get_last_successful_deployment method in Application model 2024-05-17 15:28:54 +02:00
Andras Bacsai
3281502c25 feat: Update healthcheck test in StartMongodb action 2024-05-17 14:35:37 +02:00
Andras Bacsai
ca35e536db chore: Update version to 4.0.0-beta.283 2024-05-17 14:35:31 +02:00
Andras Bacsai
bb8e0eb7bf Merge pull request #2232 from coollabsio/next
quickfixes
2024-05-17 13:44:12 +02:00
Andras Bacsai
bb451ac3b5 Refactor BackupExecutions.php to use optional chaining for deleting failed backup executions 2024-05-17 13:43:36 +02:00
Andras Bacsai
b0b7842f9c Refactor BackupEdit component to handle null s3_storage_id 2024-05-17 13:43:21 +02:00
Andras Bacsai
aad661cb65 Merge pull request #2231 from coollabsio/next
v4.0.0-beta.282
2024-05-17 13:41:33 +02:00
Andras Bacsai
ed9b63520d Refactor gitCommitLink method in Application model 2024-05-17 13:40:28 +02:00
Andras Bacsai
5de1246827 Merge pull request #2230 from coollabsio/next
v4.0.0-beta.281
2024-05-17 12:45:26 +02:00
Andras Bacsai
edc3b014cd fix: telegram group chat notifications 2024-05-17 12:09:22 +02:00
Andras Bacsai
fec98f45ce feat: Improve sorting of environment variables in the All component 2024-05-17 11:40:32 +02:00
Andras Bacsai
94810d5066 fix: use rc in hc 2024-05-17 11:39:26 +02:00
Andras Bacsai
431cc796d8 feat: sort envs alphabetically and creation date 2024-05-17 11:10:57 +02:00
Andras Bacsai
e9d2dbcc92 Refactor Upgrade.php and add isDev condition for upgrade availability 2024-05-17 10:12:17 +02:00
Andras Bacsai
7a618ef89c feat: Add lastDeploymentInfo and lastDeploymentLink props to breadcrumbs and status components 2024-05-17 10:12:13 +02:00
Andras Bacsai
5b56249d12 Refactor gitCommitLink method in Application model 2024-05-17 10:12:05 +02:00
Andras Bacsai
e2ba5abe76 fix: hc from localhost to 127.0.0.1 2024-05-17 10:11:55 +02:00
Andras Bacsai
70a4b7c863 feat: new manual update process + remove next_channel 2024-05-17 09:52:19 +02:00
Andras Bacsai
10fde1b1ef feat: shows the latest deployment commit + message on status 2024-05-17 08:53:25 +02:00
Andras Bacsai
6131746180 Add all_servers property to Kernel class for better code organization 2024-05-16 21:36:01 +02:00
Andras Bacsai
bf953bf1b5 Update version numbers 2024-05-16 21:35:57 +02:00
Andras Bacsai
d81da10dfa Merge pull request #2225 from coollabsio/next
v4.0.0-beta.280
2024-05-16 17:25:36 +02:00
Andras Bacsai
ed9cdc1ab7 Fix issue with parsing services in ScheduledTask/All.php 2024-05-16 17:24:57 +02:00
Andras Bacsai
9f506eb83a fix: commit message length 2024-05-16 17:22:32 +02:00
Andras Bacsai
36e8c52b01 Update version numbers to 4.0.0-beta.280 2024-05-16 17:21:53 +02:00
Andras Bacsai
e6d1233bfe Merge pull request #2220 from coollabsio/next
v4.0.0-beta.279
2024-05-16 13:34:33 +02:00
Andras Bacsai
6847459022 chore: Limit commit message length to 50 characters in ApplicationDeploymentJob 2024-05-16 13:33:35 +02:00
Andras Bacsai
fbf64f8037 Refactor ApplicationDeploymentJob to conditionally notify team on DeploymentSuccess 2024-05-16 11:25:58 +02:00
Andras Bacsai
2b74ca2746 Refactor scheduling of container status and log drain checks 2024-05-16 11:23:31 +02:00
Andras Bacsai
b106a82308 chore: Update version numbers to 4.0.0-beta.279 2024-05-16 11:23:26 +02:00
Andras Bacsai
5700f2f78a Merge pull request #2205 from coollabsio/next
v4.0.0-beta.278
2024-05-16 10:20:00 +02:00
Andras Bacsai
c5ca6abb90 Refactor sleep duration in check_resources method 2024-05-16 10:08:13 +02:00
Andras Bacsai
6826b6e1f8 Refactor container status and log drain checks scheduling 2024-05-16 10:08:00 +02:00
Andras Bacsai
27c4fa2fcf Refactor boarding index.blade.php for improved code structure and readability 2024-05-16 10:05:28 +02:00
Andras Bacsai
6ef1aff991 Refactor scheduling of container status and log drain checks 2024-05-16 09:56:38 +02:00
Andras Bacsai
57a026a7a1 Refactor modal input button title for dynamic configuration navbar 2024-05-16 09:39:29 +02:00
Andras Bacsai
cfc785358e Refactor README.md to add new sponsor logos 2024-05-16 09:11:52 +02:00
Andras Bacsai
2446dc6950 feat: toggle label escaping mechanism 2024-05-15 17:52:14 +02:00
Andras Bacsai
f98405188d Refactor shared.php to escape dollar signs in service labels 2024-05-15 15:45:56 +02:00
Andras Bacsai
a5cf24773c Refactor pricing-plans.blade.php for improved code structure and readability 2024-05-15 14:43:11 +02:00
Andras Bacsai
01c1e4f8cb chore: Update Docker and Docker Compose versions in Dockerfiles 2024-05-15 14:32:10 +02:00
Andras Bacsai
0759fb6436 chore: Update DOCKER_VERSION to 26.0 in install.sh script 2024-05-15 14:32:01 +02:00
Andras Bacsai
ed5188069b Refactor destination/all.blade.php for improved code structure and readability 2024-05-15 14:10:44 +02:00
Andras Bacsai
aa0a9bde76 Refactor storage create form for better usability and validation 2024-05-15 14:10:03 +02:00
Andras Bacsai
56a450a936 Refactor commit_message column length to 50 characters 2024-05-15 12:35:28 +02:00
Andras Bacsai
1e01106b94 chore: Remove unnecessary code for saving commit message 2024-05-15 11:53:28 +02:00
Andras Bacsai
b9a755d6d3 chore: Update ServerLimitCheckJob.php to handle missing serverLimit value 2024-05-15 11:44:42 +02:00
Andras Bacsai
1d9d6c899d Refactor User model role() method to use data_get() for better readability 2024-05-15 11:41:11 +02:00
Andras Bacsai
444dffb458 chore: Refactor GitHub app selection UI in project creation form 2024-05-15 11:34:59 +02:00
Andras Bacsai
3f1b7192ff feat: save commit message and better view on deployments 2024-05-15 11:31:03 +02:00
Andras Bacsai
b992b19c66 feat: adding new COOLIFY_ variables 2024-05-15 11:30:35 +02:00
Andras Bacsai
e341121f61 chore: Refactor deployment index.blade.php for improved readability and rollback handling 2024-05-15 10:47:57 +02:00
Andras Bacsai
cd3e2963b3 Refactor gitCommitLink method to handle different git repository formats 2024-05-15 10:45:08 +02:00
Andras Bacsai
346faf1d07 chore: Refactor applications.php to remove unused imports and improve code readability 2024-05-15 10:45:01 +02:00
Andras Bacsai
1e09b2bbd8 fix: use commit hash on webhooks 2024-05-15 10:44:45 +02:00
Andras Bacsai
0ffba45517 chore: Update twenty CRM template with environment variables and dependencies 2024-05-15 09:46:31 +02:00
Andras Bacsai
32ff346154 chore: Refactor Service.php to handle missing admin user in extraFields() method 2024-05-15 09:46:28 +02:00
Andras Bacsai
0008f44255 Merge pull request #2098 from jonkristian/main
Added twenty crm template
2024-05-15 09:11:04 +02:00
Andras Bacsai
a3c519a061 Merge pull request #2183 from Lxcasx/main
Correct repository links in source view for git SSH URLs
2024-05-15 09:08:22 +02:00
Andras Bacsai
5b8a923cb5 Merge pull request #2206 from maurobender/fix_schduled_tasks_executing_using_host_environment
Fix scheduled tasks being executed using host environment variables
2024-05-15 08:58:50 +02:00
Andras Bacsai
576cbc0b90 Merge branch 'next' of github.com:coollabsio/coolify into next 2024-05-14 15:40:47 +02:00
Andras Bacsai
e95e2cf152 feat: Add AdminRemoveUser command to remove users from the database 2024-05-14 15:40:45 +02:00
Andras Bacsai
317dc10af4 fix: improve scheduled task adding/removing 2024-05-14 15:19:28 +02:00
Andras Bacsai
f06065337c chore: Handle invalid cron strings in Kernel.php 2024-05-14 15:18:59 +02:00
Mauro E. Bender
cc870ca302 Fix scheduled tasks being executed using host environment variables 2024-05-14 14:29:27 +02:00
Andras Bacsai
80f5506b18 Merge pull request #2204 from TorstenDittmann/fix-compose-dependencies-on-previews
fix: docker compose dependencies for pr previews
2024-05-14 13:41:39 +02:00
Torsten Dittmann
5321a75272 fix: properly populating dependencies 2024-05-14 13:33:29 +02:00
Andras Bacsai
b70a78b7aa chore: Remove debug logging statements in Kernel.php 2024-05-14 13:04:17 +02:00
Andras Bacsai
5ad08791ea chore: Skip scheduled tasks if application or service is not running 2024-05-14 12:52:01 +02:00
Andras Bacsai
9dc3ec0bf8 chore: Refactor scheduled task view to improve code readability and maintainability 2024-05-14 12:45:21 +02:00
Andras Bacsai
69dd9d0cac chore: Update hover behavior and cursor style in scheduled task executions view 2024-05-14 12:45:17 +02:00
Torsten Dittmann
24f923e88e fix: docker compose dependencies for pr previews 2024-05-14 12:19:33 +02:00
Andras Bacsai
b5552a216d fix: only allow push and mr gitlab events 2024-05-14 11:55:20 +02:00
Lucas Heinschke
1988c617a0 Correct repository links in source view for git SSH URLs 2024-05-10 16:28:14 +02:00
Andras Bacsai
5e531d6f96 fix: only show realtime error on non-cloud instances 2024-05-10 12:50:39 +02:00
Andras Bacsai
1fb7e97700 Fix error handling in GetContainersStatus.php and increase length of stripe_comment field in migrations 2024-05-10 12:10:47 +02:00
Andras Bacsai
64d27156f5 chore: Update version numbers to 4.0.0-beta.278 2024-05-10 12:10:35 +02:00
Jon Kristian Nilsen
4549223d6d Storage type should be exposed. Fixed healthcheck test. 2024-05-03 12:56:49 +02:00
Jon Kristian Nilsen
91dd3468d5 Storage type should be exposed. Fixed healthcheck test. 2024-05-03 12:55:05 +02:00
Jon Kristian Nilsen
9cb981f068 Added twenty crm template + logo. 2024-04-29 13:29:42 +02:00
163 changed files with 1737 additions and 971 deletions

View File

@@ -34,7 +34,12 @@ Special thanks to our biggest sponsor, [CCCareers](https://cccareers.org/)!
## Github Sponsors ($40+)
<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://serpapi.com/?utm_source=coolify.io"><img width="60px" alt="SerpAPI" src="https://github.com/serpapi.png"/></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://www.quantcdn.io/?utm_source=coolify.io"><img src="https://github.com/quantcdn.png" width="60px" alt="QuantCDN"/></a>
<a href="https://www.runpod.io/?utm_source=coolify.io">
<svg style="width:60px;height:60px;background:#fff;" xmlns="http://www.w3.org/2000/svg" version="1.0" viewBox="0 0 200 200"><g><path d="M74.5 51.1c-25.4 14.9-27 16-29.6 20.2-1.8 3-1.9 5.3-1.9 32.3 0 21.7.3 29.4 1.3 30.6 1.9 2.5 46.7 27.9 48.5 27.6 1.5-.3 1.7-3.1 2-27.7.2-21.9 0-27.8-1.1-29.5-.8-1.2-9.9-6.8-20.2-12.6-10.3-5.8-19.4-11.5-20.2-12.7-1.8-2.6-.9-5.9 1.8-7.4 1.6-.8 6.3 0 21.8 4C87.8 78.7 98 81 99.6 81c4.4 0 49.9-25.9 49.9-28.4 0-1.6-3.4-2.8-24-8.2-13.2-3.5-25.1-6.3-26.5-6.3-1.4.1-12.4 5.9-24.5 13z"></path><path d="m137.2 68.1-3.3 2.1 6.3 3.7c3.5 2 6.3 4.3 6.3 5.1 0 .9-8 6.1-19.4 12.6-10.6 6-20 11.9-20.7 12.9-1.2 1.6-1.4 7.2-1.2 29.4.3 24.8.5 27.6 2 27.9 1.8.3 46.6-25.1 48.6-27.6.9-1.2 1.2-8.8 1.2-30.2s-.3-29-1.2-30.2c-1.6-1.9-12.1-7.8-13.9-7.8-.8 0-2.9 1-4.7 2.1z"></path></g></svg></a>
<a href="https://www.flint.sh/en/home?utm_source=coolify.io"> <img src="https://github.com/Flint-company.png" width="60px" alt="FlintCompany"/></a>
<a href="https://americancloud.com/?utm_source=coolify.io"><img src="https://github.com/American-Cloud.png" width="60px" alt="American Cloud"/></a>
<a href="https://cryptojobslist.com/?utm_source=coolify.io"><img src="https://github.com/cryptojobslist.png" width="60px" alt="CryptoJobsList" /></a>
<a href="https://www.uxwizz.com/?utm_source=coolify.io"><img width="60px" alt="UXWizz" src="https://github.com/UXWizz.png"/></a>
@@ -62,6 +67,7 @@ Special thanks to our biggest sponsor, [CCCareers](https://cccareers.org/)!
<a href="https://opencollective.com/coollabsio/organization/8/website"><img src="https://opencollective.com/coollabsio/organization/8/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/9/website"><img src="https://opencollective.com/coollabsio/organization/9/avatar.svg"></a>
## Individuals
<a href="https://opencollective.com/coollabsio"><img src="https://opencollective.com/coollabsio/individuals.svg?width=890"></a>

View File

@@ -50,8 +50,9 @@ class StartMongodb
],
'healthcheck' => [
'test' => [
'CMD-SHELL',
'mongosh --eval "printjson(db.runCommand(\"ping\"))"'
"CMD",
"echo",
"ok"
],
'interval' => '5s',
'timeout' => '5s',

View File

@@ -6,14 +6,12 @@ use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy;
use App\Actions\Shared\ComplexStatusCheck;
use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Models\Server;
use App\Models\ServiceDatabase;
use App\Notifications\Container\ContainerRestarted;
use App\Notifications\Container\ContainerStopped;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log;
use Lorisleiva\Actions\Concerns\AsAction;
class GetContainersStatus
@@ -24,9 +22,9 @@ class GetContainersStatus
public function handle(Server $server)
{
if (isDev()) {
$server = Server::find(0);
}
// if (isDev()) {
// $server = Server::find(0);
// }
$this->server = $server;
if (!$this->server->isFunctional()) {
return 'Server is not ready.';
@@ -154,7 +152,7 @@ class GetContainersStatus
if ($isPublic) {
$foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) {
if ($this->server->isSwarm()) {
// TODO: fix this with sentinel
// TODO: fix this with sentinel
return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
} else {
return data_get($value, 'name') === "$uuid-proxy";
@@ -316,7 +314,7 @@ class GetContainersStatus
$this->server->proxyType();
$foundProxyContainer = $containers->filter(function ($value, $key) {
if ($this->server->isSwarm()) {
// TODO: fix this with sentinel
// TODO: fix this with sentinel
return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik';
} else {
return data_get($value, 'name') === 'coolify-proxy';
@@ -339,7 +337,7 @@ class GetContainersStatus
instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
}
} catch (\Exception $e) {
send_internal_notification("ContainerStatusJob failed on ({$this->server->id}) with: " . $e->getMessage());
// send_internal_notification("ContainerStatusJob failed on ({$this->server->id}) with: " . $e->getMessage());
ray($e->getMessage());
return handleError($e);
}
@@ -442,19 +440,21 @@ class GetContainersStatus
if ($database_id) {
$service_db = ServiceDatabase::where('id', $database_id)->first();
if ($service_db) {
$uuid = $service_db->service->uuid;
$isPublic = data_get($service_db, 'is_public');
if ($isPublic) {
$foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) {
if ($this->server->isSwarm()) {
return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
} else {
return data_get($value, 'Name') === "/$uuid-proxy";
$uuid = data_get($service_db, 'service.uuid');
if ($uuid) {
$isPublic = data_get($service_db, 'is_public');
if ($isPublic) {
$foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) {
if ($this->server->isSwarm()) {
return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
} else {
return data_get($value, 'Name') === "/$uuid-proxy";
}
})->first();
if (!$foundTcpProxy) {
StartDatabaseProxy::run($service_db);
// $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$service_db->service->name}", $this->server));
}
})->first();
if (!$foundTcpProxy) {
StartDatabaseProxy::run($service_db);
// $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$service_db->service->name}", $this->server));
}
}
}

View File

@@ -27,10 +27,10 @@ class UpdateCoolify
CleanupDocker::run($this->server, false);
$this->latestVersion = get_latest_version_of_coolify();
$this->currentVersion = config('version');
if ($settings->next_channel) {
ray('next channel enabled');
$this->latestVersion = 'next';
}
// if ($settings->next_channel) {
// ray('next channel enabled');
// $this->latestVersion = 'next';
// }
if ($force) {
$this->update();
} else {

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Console\Commands;
use App\Models\Server;
use App\Models\User;
use Illuminate\Console\Command;
class AdminRemoveUser extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'admin:remove-user {email}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Remove User from database';
/**
* Execute the console command.
*/
public function handle()
{
try {
$email = $this->argument('email');
$confirm = $this->confirm('Are you sure you want to remove user with email: ' . $email . '?');
if (!$confirm) {
$this->info('User removal cancelled.');
return;
}
$this->info("Removing user with email: $email");
$user = User::whereEmail($email)->firstOrFail();
$teams = $user->teams;
foreach ($teams as $team) {
if ($team->members->count() > 1) {
$this->error('User is a member of a team with more than one member. Please remove user from team first.');
return;
}
$team->delete();
}
$user->delete();
} catch (\Exception $e) {
$this->error('Failed to remove user.');
$this->error($e->getMessage());
return;
}
}
}

View File

@@ -1,33 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Server;
use Illuminate\Console\Command;
class Cloud extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'cloud:unused-servers';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Get Unused Servers from Cloud';
/**
* Execute the console command.
*/
public function handle()
{
Server::all()->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended',true)->each(function($server){
$this->info($server->name);
});
}
}

View File

@@ -29,7 +29,6 @@ class RootResetPassword extends Command
*/
public function handle()
{
//
$this->info('You are about to reset the root password.');
$password = password('Give me a new password for root user: ');
$passwordAgain = password('Again');

View File

@@ -21,8 +21,10 @@ use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
private $all_servers;
protected function schedule(Schedule $schedule): void
{
$this->all_servers = Server::all();
if (isDev()) {
// Instance Jobs
$schedule->command('horizon:snapshot')->everyMinute();
@@ -56,7 +58,7 @@ class Kernel extends ConsoleKernel
}
private function pull_helper_image($schedule)
{
$servers = Server::all()->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4');
$servers = $this->all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4');
foreach ($servers as $server) {
if (config('coolify.is_sentinel_enabled')) {
$schedule->job(new PullSentinelImageJob($server))->everyFiveMinutes()->onOneServer();
@@ -67,12 +69,12 @@ class Kernel extends ConsoleKernel
private function check_resources($schedule)
{
if (isCloud()) {
$servers = Server::all()->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended', false)->where('ip', '!=', '1.2.3.4');
$servers = $this->all_servers->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended', false)->where('ip', '!=', '1.2.3.4');
$own = Team::find(0)->servers;
$servers = $servers->merge($own);
$containerServers = $servers->where('settings.is_swarm_worker', false)->where('settings.is_build_server', false);
} else {
$servers = Server::all()->where('ip', '!=', '1.2.3.4');
$servers = $this->all_servers->where('ip', '!=', '1.2.3.4');
$containerServers = $servers->where('settings.is_swarm_worker', false)->where('settings.is_build_server', false);
}
foreach ($containerServers as $server) {
@@ -138,7 +140,16 @@ class Kernel extends ConsoleKernel
$scheduled_task->delete();
continue;
}
if ($application) {
if (str($application->status)->contains('running') === false) {
continue;
}
}
if ($service) {
if (str($service->status())->contains('running') === false) {
continue;
}
}
if (isset(VALID_CRON_STRINGS[$scheduled_task->frequency])) {
$scheduled_task->frequency = VALID_CRON_STRINGS[$scheduled_task->frequency];
}

View File

@@ -47,7 +47,7 @@ class Bitbucket extends Controller
if ($x_bitbucket_event === 'repo:push') {
$branch = data_get($payload, 'push.changes.0.new.name');
$full_name = data_get($payload, 'repository.full_name');
$commit = data_get($payload, 'push.changes.0.new.target.hash');
if (!$branch) {
return response([
'status' => 'failed',
@@ -104,6 +104,7 @@ class Bitbucket extends Controller
queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
commit: $commit,
force_rebuild: false,
is_webhook: true
);

View File

@@ -129,6 +129,7 @@ class Github extends Controller
application: $application,
deployment_uuid: $deployment_uuid,
force_rebuild: false,
commit: data_get($payload, 'after', 'HEAD'),
is_webhook: true,
);
$return_payloads->push([
@@ -177,6 +178,7 @@ class Github extends Controller
pull_request_id: $pull_request_id,
deployment_uuid: $deployment_uuid,
force_rebuild: false,
commit: data_get($payload, 'head.sha', 'HEAD'),
is_webhook: true,
git_type: 'github'
);
@@ -338,6 +340,7 @@ class Github extends Controller
queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
commit: data_get($payload, 'after', 'HEAD'),
force_rebuild: false,
is_webhook: true,
);
@@ -387,6 +390,7 @@ class Github extends Controller
pull_request_id: $pull_request_id,
deployment_uuid: $deployment_uuid,
force_rebuild: false,
commit: data_get($payload, 'head.sha', 'HEAD'),
is_webhook: true,
git_type: 'github'
);

View File

@@ -38,6 +38,15 @@ class Gitlab extends Controller
$headers = $request->headers->all();
$x_gitlab_token = data_get($headers, 'x-gitlab-token.0');
$x_gitlab_event = data_get($payload, 'object_kind');
$allowed_events = ['push', 'merge_request'];
if (!in_array($x_gitlab_event, $allowed_events)) {
$return_payloads->push([
'status' => 'failed',
'message' => 'Event not allowed. Only push and merge_request events are allowed.',
]);
return response($return_payloads);
}
if ($x_gitlab_event === 'push') {
$branch = data_get($payload, 'ref');
$full_name = data_get($payload, 'project.path_with_namespace');
@@ -124,6 +133,7 @@ class Gitlab extends Controller
queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
commit: data_get($payload, 'after', 'HEAD'),
force_rebuild: false,
is_webhook: true,
);
@@ -173,6 +183,7 @@ class Gitlab extends Controller
application: $application,
pull_request_id: $pull_request_id,
deployment_uuid: $deployment_uuid,
commit: data_get($payload, 'object_attributes.last_commit.id', 'HEAD'),
force_rebuild: false,
is_webhook: true,
git_type: 'gitlab'

View File

@@ -107,6 +107,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private ?string $fullRepoUrl = null;
private ?string $branch = null;
private ?string $coolify_variables = null;
public $tries = 1;
public function __construct(int $application_deployment_queue_id)
{
@@ -406,7 +408,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
);
} 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} build"), "hidden" => true],
[executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build"), "hidden" => true],
);
}
@@ -436,9 +438,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} else {
$this->write_deployment_configurations();
$server_workdir = $this->application->workdir();
ray("SOURCE_COMMIT={$this->commit} docker compose --project-directory {$server_workdir} -f {$server_workdir}{$this->docker_compose_location} up -d");
ray("{$this->coolify_variables} docker compose --project-directory {$server_workdir} -f {$server_workdir}{$this->docker_compose_location} up -d");
$this->execute_remote_command(
["SOURCE_COMMIT={$this->commit} docker compose --project-directory {$server_workdir} -f {$server_workdir}{$this->docker_compose_location} up -d", "hidden" => true],
["{$this->coolify_variables} docker compose --project-directory {$server_workdir} -f {$server_workdir}{$this->docker_compose_location} up -d", "hidden" => true],
);
}
} else {
@@ -449,7 +451,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->write_deployment_configurations();
} else {
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up -d"), "hidden" => true],
[executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up -d"), "hidden" => true],
);
$this->write_deployment_configurations();
}
@@ -711,10 +713,40 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function save_environment_variables()
{
$envs = collect([]);
$local_branch = $this->branch;
if ($this->pull_request_id !== 0) {
$local_branch = "pull/{$this->pull_request_id}/head";
}
$sort = $this->application->settings->is_env_sorting_enabled;
if ($sort) {
$sorted_environment_variables = $this->application->environment_variables->sortBy('key');
$sorted_environment_variables_preview = $this->application->environment_variables_preview->sortBy('key');
} else {
$sorted_environment_variables = $this->application->environment_variables->sortBy('id');
$sorted_environment_variables_preview = $this->application->environment_variables_preview->sortBy('id');
}
$ports = $this->application->main_port();
if ($this->pull_request_id !== 0) {
$this->env_filename = ".env-pr-$this->pull_request_id";
foreach ($this->application->environment_variables_preview as $env) {
// Add SOURCE_COMMIT if not exists
if ($this->application->environment_variables_preview->where('key', 'SOURCE_COMMIT')->isEmpty()) {
if (!is_null($this->commit)) {
$envs->push("SOURCE_COMMIT={$this->commit}");
} else {
$envs->push("SOURCE_COMMIT=unknown");
}
}
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_FQDN')->isEmpty()) {
$envs->push("COOLIFY_FQDN={$this->preview->fqdn}");
}
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_URL')->isEmpty()) {
$url = str($this->preview->fqdn)->replace('http://', '')->replace('https://', '');
$envs->push("COOLIFY_URL={$url}");
}
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
$envs->push("COOLIFY_BRANCH={$local_branch}");
}
foreach ($sorted_environment_variables_preview as $env) {
$real_value = $env->real_value;
if ($env->version === '4.0.0-beta.239') {
$real_value = $env->real_value;
@@ -735,20 +767,27 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($this->application->environment_variables_preview->where('key', 'HOST')->isEmpty()) {
$envs->push("HOST=0.0.0.0");
}
} else {
$this->env_filename = ".env";
// Add SOURCE_COMMIT if not exists
if ($this->application->environment_variables_preview->where('key', 'SOURCE_COMMIT')->isEmpty()) {
if ($this->application->environment_variables->where('key', 'SOURCE_COMMIT')->isEmpty()) {
if (!is_null($this->commit)) {
$envs->push("SOURCE_COMMIT={$this->commit}");
} else {
$envs->push("SOURCE_COMMIT=unknown");
}
}
$envs = $envs->sort(function ($a, $b) {
return strpos($a, '$') === false ? -1 : 1;
});
} else {
$this->env_filename = ".env";
foreach ($this->application->environment_variables as $env) {
if ($this->application->environment_variables->where('key', 'COOLIFY_FQDN')->isEmpty()) {
$envs->push("COOLIFY_FQDN={$this->application->fqdn}");
}
if ($this->application->environment_variables->where('key', 'COOLIFY_URL')->isEmpty()) {
$url = str($this->application->fqdn)->replace('http://', '')->replace('https://', '');
$envs->push("COOLIFY_URL={$url}");
}
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
$envs->push("COOLIFY_BRANCH={$local_branch}");
}
foreach ($sorted_environment_variables as $env) {
$real_value = $env->real_value;
if ($env->version === '4.0.0-beta.239') {
$real_value = $env->real_value;
@@ -769,17 +808,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($this->application->environment_variables->where('key', 'HOST')->isEmpty()) {
$envs->push("HOST=0.0.0.0");
}
// Add SOURCE_COMMIT if not exists
if ($this->application->environment_variables->where('key', 'SOURCE_COMMIT')->isEmpty()) {
if (!is_null($this->commit)) {
$envs->push("SOURCE_COMMIT={$this->commit}");
} else {
$envs->push("SOURCE_COMMIT=unknown");
}
}
$envs = $envs->sort(function ($a, $b) {
return strpos($a, '$') === false ? -1 : 1;
});
}
if ($envs->isEmpty()) {
@@ -935,9 +963,23 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
"save" => "health_check",
"append" => false
],
[
"docker inspect --format='{{json .State.Health.Log}}' {$this->container_name}",
"hidden" => true,
"save" => "health_check_logs",
"append" => false
],
);
$this->application_deployment_queue->addLogEntry("Attempt {$counter} of {$this->application->health_check_retries} | Healthcheck status: {$this->saved_outputs->get('health_check')}");
$health_check_logs = data_get(collect(json_decode($this->saved_outputs->get('health_check_logs')))->last(), 'Output', '(no logs)');
if (empty($health_check_logs)) {
$health_check_logs = '(no logs)';
}
$health_check_return_code = data_get(collect(json_decode($this->saved_outputs->get('health_check_logs')))->last(), 'ExitCode', '(no return code)');
if ($health_check_logs !== '(no logs)' || $health_check_return_code !== '(no return code)') {
$this->application_deployment_queue->addLogEntry("Healthcheck logs: {$health_check_logs} | Return code: {$health_check_return_code}");
}
if (Str::of($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'healthy') {
$this->newVersionIsHealthy = true;
$this->application->update(['status' => 'running']);
@@ -971,6 +1013,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->generate_image_names();
$this->application_deployment_queue->addLogEntry("Starting pull request (#{$this->pull_request_id}) deployment of {$this->customRepository}:{$this->application->git_branch}.");
$this->prepare_builder_image();
$this->check_git_if_build_needed();
$this->clone_repository();
$this->set_base_dir();
$this->cleanup_git();
@@ -1080,9 +1123,30 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
{
$this->application_deployment_queue->addLogEntry("Setting base directory to {$this->workdir}.");
}
private function set_coolify_variables()
{
$this->coolify_variables = "SOURCE_COMMIT={$this->commit} ";
if ($this->pull_request_id === 0) {
$fqdn = $this->application->fqdn;
} else {
$fqdn = $this->preview->fqdn;
}
if (isset($fqdn)) {
$this->coolify_variables .= "COOLIFY_FQDN={$fqdn} ";
$url = str($fqdn)->replace('http://', '')->replace('https://', '');
$this->coolify_variables .= "COOLIFY_URL={$url} ";
}
if (isset($this->application->git_branch)) {
$this->coolify_variables .= "COOLIFY_BRANCH={$this->application->git_branch} ";
}
}
private function check_git_if_build_needed()
{
$this->generate_git_import_commands();
$local_branch = $this->branch;
if ($this->pull_request_id !== 0) {
$local_branch = "pull/{$this->pull_request_id}/head";
}
$private_key = data_get($this->application, 'private_key.private_key');
if ($private_key) {
$private_key = base64_encode($private_key);
@@ -1097,7 +1161,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
executeInDocker($this->deployment_uuid, "chmod 600 /root/.ssh/id_rsa")
],
[
executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git ls-remote {$this->fullRepoUrl} {$this->branch}"),
executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git ls-remote {$this->fullRepoUrl} {$local_branch}"),
"hidden" => true,
"save" => "git_commit_sha"
],
@@ -1105,15 +1169,19 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} else {
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git ls-remote {$this->fullRepoUrl} {$this->branch}"),
executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git ls-remote {$this->fullRepoUrl} {$local_branch}"),
"hidden" => true,
"save" => "git_commit_sha"
],
);
}
ray("GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git ls-remote {$this->fullRepoUrl} {$local_branch}");
if ($this->saved_outputs->get('git_commit_sha') && !$this->rollback) {
$this->commit = $this->saved_outputs->get('git_commit_sha')->before("\t");
$this->application_deployment_queue->commit = $this->commit;
$this->application_deployment_queue->save();
}
$this->set_coolify_variables();
}
private function clone_repository()
{
@@ -1123,12 +1191,29 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($this->pull_request_id !== 0) {
$this->application_deployment_queue->addLogEntry("Checking out tag pull/{$this->pull_request_id}/head.");
}
ray($importCommands);
$this->execute_remote_command(
[
$importCommands, "hidden" => true
]
);
$this->create_workdir();
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "cd {$this->workdir} && git log -1 {$this->commit} --pretty=%B"),
"hidden" => true,
"save" => "commit_message"
]
);
ray($this->saved_outputs->get('commit_message'));
raY($this->commit);
if ($this->saved_outputs->get('commit_message')) {
$commit_message = str($this->saved_outputs->get('commit_message'))->limit(47);
$this->application_deployment_queue->commit_message = $commit_message->value();
ApplicationDeploymentQueue::whereCommit($this->commit)->whereApplicationId($this->application->id)->update(
['commit_message' => $commit_message->value()]
);
}
}
private function generate_git_import_commands()
@@ -1216,6 +1301,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function generate_env_variables()
{
$this->env_args = collect([]);
$this->env_args->put('SOURCE_COMMIT', $this->commit);
if ($this->pull_request_id === 0) {
foreach ($this->application->build_environment_variables as $env) {
if (!is_null($env->real_value)) {
@@ -1229,7 +1315,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
}
}
}
$this->env_args->put('SOURCE_COMMIT', $this->commit);
}
private function generate_compose_file()
@@ -1277,9 +1362,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($this->pull_request_id !== 0) {
$labels = collect(generateLabelsApplication($this->application, $this->preview));
}
$labels = $labels->map(function ($value, $key) {
return escapeDollarSign($value);
});
if ($this->application->settings->is_container_label_escape_enabled) {
$labels = $labels->map(function ($value, $key) {
return escapeDollarSign($value);
});
}
$labels = $labels->merge(defaultLabels($this->application->id, $this->application->uuid, $this->pull_request_id))->toArray();
// Check for custom HEALTHCHECK
@@ -1767,11 +1854,11 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$this->application_deployment_queue->addLogEntry("Pulling latest images from the registry.");
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} pull"), "hidden" => true],
[executeInDocker($this->deployment_uuid, "SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->workdir} build"), "hidden" => true],
[executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} build"), "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} build"), "hidden" => true],
[executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build"), "hidden" => true],
);
}
$this->application_deployment_queue->addLogEntry("New images built.");
@@ -1783,16 +1870,16 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$this->application_deployment_queue->addLogEntry("Pulling latest images from the registry.");
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} pull"), "hidden" => true],
[executeInDocker($this->deployment_uuid, "SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true],
[executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true],
);
} else {
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],
["{$this->coolify_variables} 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],
[executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up --build -d"), "hidden" => true],
);
}
}

View File

@@ -37,336 +37,5 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
public function handle()
{
GetContainersStatus::run($this->server);
return;
// if (!$this->server->isFunctional()) {
// return 'Server is not ready.';
// };
// $applications = $this->server->applications();
// $skip_these_applications = collect([]);
// foreach ($applications as $application) {
// if ($application->additional_servers->count() > 0) {
// $skip_these_applications->push($application);
// ComplexStatusCheck::run($application);
// $applications = $applications->filter(function ($value, $key) use ($application) {
// return $value->id !== $application->id;
// });
// }
// }
// $applications = $applications->filter(function ($value, $key) use ($skip_these_applications) {
// return !$skip_these_applications->pluck('id')->contains($value->id);
// });
// try {
// if ($this->server->isSwarm()) {
// $containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this->server, false);
// $containerReplicates = instant_remote_process(["docker service ls --format '{{json .}}'"], $this->server, false);
// } else {
// // Precheck for containers
// $containers = instant_remote_process(["docker container ls -q"], $this->server, false);
// if (!$containers) {
// return;
// }
// $containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server, false);
// $containerReplicates = null;
// }
// if (is_null($containers)) {
// return;
// }
// $containers = format_docker_command_output_to_json($containers);
// if ($containerReplicates) {
// $containerReplicates = format_docker_command_output_to_json($containerReplicates);
// foreach ($containerReplicates as $containerReplica) {
// $name = data_get($containerReplica, 'Name');
// $containers = $containers->map(function ($container) use ($name, $containerReplica) {
// if (data_get($container, 'Spec.Name') === $name) {
// $replicas = data_get($containerReplica, 'Replicas');
// $running = str($replicas)->explode('/')[0];
// $total = str($replicas)->explode('/')[1];
// if ($running === $total) {
// data_set($container, 'State.Status', 'running');
// data_set($container, 'State.Health.Status', 'healthy');
// } else {
// data_set($container, 'State.Status', 'starting');
// data_set($container, 'State.Health.Status', 'unhealthy');
// }
// }
// return $container;
// });
// }
// }
// $databases = $this->server->databases();
// $services = $this->server->services()->get();
// $previews = $this->server->previews();
// $foundApplications = [];
// $foundApplicationPreviews = [];
// $foundDatabases = [];
// $foundServices = [];
// foreach ($containers as $container) {
// if ($this->server->isSwarm()) {
// $labels = data_get($container, 'Spec.Labels');
// $uuid = data_get($labels, 'coolify.name');
// } else {
// $labels = data_get($container, 'Config.Labels');
// }
// $containerStatus = data_get($container, 'State.Status');
// $containerHealth = data_get($container, 'State.Health.Status', 'unhealthy');
// $containerStatus = "$containerStatus ($containerHealth)";
// $labels = Arr::undot(format_docker_labels_to_json($labels));
// $applicationId = data_get($labels, 'coolify.applicationId');
// if ($applicationId) {
// $pullRequestId = data_get($labels, 'coolify.pullRequestId');
// if ($pullRequestId) {
// if (str($applicationId)->contains('-')) {
// $applicationId = str($applicationId)->before('-');
// }
// $preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->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', $applicationId)->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');
// $type = data_get($labels, 'coolify.type');
// if ($uuid) {
// if ($type === 'service') {
// $database_id = data_get($labels, 'coolify.service.subId');
// if ($database_id) {
// $service_db = ServiceDatabase::where('id', $database_id)->first();
// if ($service_db) {
// $uuid = $service_db->service->uuid;
// $isPublic = data_get($service_db, 'is_public');
// if ($isPublic) {
// $foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) {
// if ($this->server->isSwarm()) {
// return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
// } else {
// return data_get($value, 'Name') === "/$uuid-proxy";
// }
// })->first();
// if (!$foundTcpProxy) {
// StartDatabaseProxy::run($service_db);
// // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$service_db->service->name}", $this->server));
// }
// }
// }
// }
// } else {
// $database = $databases->where('uuid', $uuid)->first();
// if ($database) {
// $isPublic = data_get($database, 'is_public');
// $foundDatabases[] = $database->id;
// $statusFromDb = $database->status;
// if ($statusFromDb !== $containerStatus) {
// $database->update(['status' => $containerStatus]);
// }
// if ($isPublic) {
// $foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) {
// if ($this->server->isSwarm()) {
// return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
// } else {
// return data_get($value, 'Name') === "/$uuid-proxy";
// }
// })->first();
// if (!$foundTcpProxy) {
// StartDatabaseProxy::run($database);
// $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server));
// }
// }
// } else {
// // Notify user that this container should not be there.
// }
// }
// }
// if (data_get($container, 'Name') === '/coolify-db') {
// $foundDatabases[] = 0;
// }
// }
// $serviceLabelId = data_get($labels, 'coolify.serviceId');
// if ($serviceLabelId) {
// $subType = data_get($labels, 'coolify.service.subType');
// $subId = data_get($labels, 'coolify.service.subId');
// $service = $services->where('id', $serviceLabelId)->first();
// if (!$service) {
// continue;
// }
// if ($subType === 'application') {
// $service = $service->applications()->where('id', $subId)->first();
// } else {
// $service = $service->databases()->where('id', $subId)->first();
// }
// if ($service) {
// $foundServices[] = "$service->id-$service->name";
// $statusFromDb = $service->status;
// if ($statusFromDb !== $containerStatus) {
// // ray('Updating status: ' . $containerStatus);
// $service->update(['status' => $containerStatus]);
// }
// }
// }
// }
// $exitedServices = collect([]);
// foreach ($services as $service) {
// $apps = $service->applications()->get();
// $dbs = $service->databases()->get();
// foreach ($apps as $app) {
// if (in_array("$app->id-$app->name", $foundServices)) {
// continue;
// } else {
// $exitedServices->push($app);
// }
// }
// foreach ($dbs as $db) {
// if (in_array("$db->id-$db->name", $foundServices)) {
// continue;
// } else {
// $exitedServices->push($db);
// }
// }
// }
// $exitedServices = $exitedServices->unique('id');
// foreach ($exitedServices as $exitedService) {
// if (str($exitedService->status)->startsWith('exited')) {
// continue;
// }
// $name = data_get($exitedService, 'name');
// $fqdn = data_get($exitedService, 'fqdn');
// $containerName = $name ? "$name, available at $fqdn" : $fqdn;
// $projectUuid = data_get($service, 'environment.project.uuid');
// $serviceUuid = data_get($service, 'uuid');
// $environmentName = data_get($service, 'environment.name');
// if ($projectUuid && $serviceUuid && $environmentName) {
// $url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/service/" . $serviceUuid;
// } else {
// $url = null;
// }
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
// $exitedService->update(['status' => 'exited']);
// }
// $notRunningApplications = $applications->pluck('id')->diff($foundApplications);
// foreach ($notRunningApplications as $applicationId) {
// $application = $applications->where('id', $applicationId)->first();
// if (str($application->status)->startsWith('exited')) {
// continue;
// }
// $application->update(['status' => 'exited']);
// $name = data_get($application, 'name');
// $fqdn = data_get($application, 'fqdn');
// $containerName = $name ? "$name ($fqdn)" : $fqdn;
// $projectUuid = data_get($application, 'environment.project.uuid');
// $applicationUuid = data_get($application, 'uuid');
// $environment = data_get($application, 'environment.name');
// if ($projectUuid && $applicationUuid && $environment) {
// $url = base_url() . '/project/' . $projectUuid . "/" . $environment . "/application/" . $applicationUuid;
// } else {
// $url = null;
// }
// $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 (str($preview->status)->startsWith('exited')) {
// continue;
// }
// $preview->update(['status' => 'exited']);
// $name = data_get($preview, 'name');
// $fqdn = data_get($preview, 'fqdn');
// $containerName = $name ? "$name ($fqdn)" : $fqdn;
// $projectUuid = data_get($preview, 'application.environment.project.uuid');
// $environmentName = data_get($preview, 'application.environment.name');
// $applicationUuid = data_get($preview, 'application.uuid');
// if ($projectUuid && $applicationUuid && $environmentName) {
// $url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/application/" . $applicationUuid;
// } else {
// $url = null;
// }
// $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 (str($database->status)->startsWith('exited')) {
// continue;
// }
// $database->update(['status' => 'exited']);
// $name = data_get($database, 'name');
// $fqdn = data_get($database, 'fqdn');
// $containerName = $name;
// $projectUuid = data_get($database, 'environment.project.uuid');
// $environmentName = data_get($database, 'environment.name');
// $databaseUuid = data_get($database, 'uuid');
// if ($projectUuid && $databaseUuid && $environmentName) {
// $url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/database/" . $databaseUuid;
// } else {
// $url = null;
// }
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
// }
// // Check if proxy is running
// $this->server->proxyType();
// $foundProxyContainer = $containers->filter(function ($value, $key) {
// if ($this->server->isSwarm()) {
// return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik';
// } else {
// return data_get($value, 'Name') === '/coolify-proxy';
// }
// })->first();
// if (!$foundProxyContainer) {
// try {
// $shouldStart = CheckProxy::run($this->server);
// if ($shouldStart) {
// StartProxy::run($this->server, false);
// $this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server));
// }
// } catch (\Throwable $e) {
// ray($e);
// }
// } else {
// $this->server->proxy->status = data_get($foundProxyContainer, 'State.Status');
// $this->server->save();
// $connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
// instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
// }
// } catch (\Throwable $e) {
// send_internal_notification("ContainerStatusJob failed on ({$this->server->id}) with: " . $e->getMessage());
// ray($e->getMessage());
// return handleError($e);
// }
}
}

View File

@@ -8,14 +8,13 @@ use App\Models\Server;
use App\Models\Application;
use App\Models\Service;
use App\Models\Team;
use App\Notifications\ScheduledTask\TaskFailed;
use Illuminate\Bus\Queueable;
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 Throwable;
class ScheduledTaskJob implements ShouldQueue
{
@@ -77,8 +76,12 @@ class ScheduledTaskJob implements ShouldQueue
$this->containers[] = 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[] = data_get($database, 'name') . '-' . data_get($this->resource, 'uuid');
}
});
}
if (count($this->containers) == 0) {
throw new \Exception('ScheduledTaskJob failed: No containers running.');
}
@@ -89,7 +92,7 @@ class ScheduledTaskJob implements ShouldQueue
foreach ($this->containers as $containerName) {
if (count($this->containers) == 1 || str_starts_with($containerName, $this->task->container . '-' . $this->resource->uuid)) {
$cmd = 'sh -c "' . str_replace('"', '\"', $this->task->command) . '"';
$cmd = "sh -c '" . str_replace("'", "'\''", $this->task->command) . "'";
$exec = "docker exec {$containerName} {$cmd}";
$this->task_output = instant_remote_process([$exec], $this->server, true);
$this->task_log->update([
@@ -110,6 +113,7 @@ class ScheduledTaskJob implements ShouldQueue
'message' => $this->task_output ?? $e->getMessage(),
]);
}
$this->team?->notify(new TaskFailed($this->task, $e->getMessage()));
// send_internal_notification('ScheduledTaskJob failed with: ' . $e->getMessage());
throw $e;
}

View File

@@ -41,7 +41,6 @@ class SendMessageToDiscordJob implements ShouldQueue, ShouldBeEncrypted
$payload = [
'content' => $this->text,
];
ray($payload);
Http::post($this->webhookUrl, $payload);
}
}

View File

@@ -40,7 +40,7 @@ class ServerLimitCheckJob implements ShouldQueue, ShouldBeEncrypted
try {
$servers = $this->team->servers;
$servers_count = $servers->count();
$limit = $this->team->limits['serverLimit'];
$limit = data_get($this->team->limits, 'serverLimit', 2);
$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) {

View File

@@ -52,6 +52,9 @@ class Index extends Component
public function mount()
{
if (auth()->user()?->isMember() && auth()->user()->currentTeam()->show_boarding === true) {
return redirect()->route('dashboard');
}
$this->privateKeyName = generate_random_name();
$this->remoteServerName = generate_random_name();
if (isDev()) {

View File

@@ -16,6 +16,7 @@ class Discord extends Component
'team.discord_notifications_deployments' => 'nullable|boolean',
'team.discord_notifications_status_changes' => 'nullable|boolean',
'team.discord_notifications_database_backups' => 'nullable|boolean',
'team.discord_notifications_scheduled_tasks' => 'nullable|boolean',
];
protected $validationAttributes = [
'team.discord_webhook_url' => 'Discord Webhook',

View File

@@ -28,6 +28,7 @@ class Email extends Component
'team.smtp_notifications_deployments' => 'nullable|boolean',
'team.smtp_notifications_status_changes' => 'nullable|boolean',
'team.smtp_notifications_database_backups' => 'nullable|boolean',
'team.smtp_notifications_scheduled_tasks' => 'nullable|boolean',
'team.use_instance_email_settings' => 'boolean',
'team.resend_enabled' => 'nullable|boolean',
'team.resend_api_key' => 'nullable',

View File

@@ -18,10 +18,12 @@ class Telegram extends Component
'team.telegram_notifications_deployments' => 'nullable|boolean',
'team.telegram_notifications_status_changes' => 'nullable|boolean',
'team.telegram_notifications_database_backups' => 'nullable|boolean',
'team.telegram_notifications_scheduled_tasks' => 'nullable|boolean',
'team.telegram_notifications_test_message_thread_id' => 'nullable|string',
'team.telegram_notifications_deployments_message_thread_id' => 'nullable|string',
'team.telegram_notifications_status_changes_message_thread_id' => 'nullable|string',
'team.telegram_notifications_database_backups_message_thread_id' => 'nullable|string',
'team.telegram_notifications_scheduled_tasks_thread_id' => 'nullable|string',
];
protected $validationAttributes = [
'team.telegram_token' => 'Token',

View File

@@ -22,6 +22,7 @@ class General extends Component
public ?string $git_commit_sha = null;
public string $build_pack;
public ?string $ports_exposes = null;
public bool $is_container_label_escape_enabled = true;
public $customLabels;
public bool $labelsChanged = false;
@@ -30,7 +31,7 @@ class General extends Component
public ?string $initialDockerComposeLocation = null;
public ?string $initialDockerComposePrLocation = null;
public $parsedServices = [];
public null|Collection $parsedServices;
public $parsedServiceDomains = [];
protected $listeners = [
@@ -74,6 +75,7 @@ class General extends Component
'application.post_deployment_command_container' => 'nullable',
'application.settings.is_static' => 'boolean|required',
'application.settings.is_build_server_enabled' => 'boolean|required',
'application.settings.is_container_label_escape_enabled' => 'boolean|required',
'application.watch_paths' => 'nullable',
];
protected $validationAttributes = [
@@ -109,12 +111,17 @@ class General extends Component
'application.docker_compose_custom_build_command' => 'Docker compose custom build command',
'application.settings.is_static' => 'Is static',
'application.settings.is_build_server_enabled' => 'Is build server enabled',
'application.settings.is_container_label_escape_enabled' => 'Is container label escape enabled',
'application.watch_paths' => 'Watch paths',
];
public function mount()
{
try {
$this->parsedServices = $this->application->parseCompose();
if (is_null($this->parsedServices) || empty($this->parsedServices)) {
$this->dispatch('error', "Failed to parse your docker-compose file. Please check the syntax and try again.");
return;
}
} catch (\Throwable $e) {
$this->dispatch('error', $e->getMessage());
}
@@ -124,6 +131,7 @@ class General extends Component
}
$this->parsedServiceDomains = $this->application->docker_compose_domains ? json_decode($this->application->docker_compose_domains, true) : [];
$this->ports_exposes = $this->application->ports_exposes;
$this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled;
$this->customLabels = $this->application->parseContainerLabels();
if (!$this->customLabels && $this->application->destination->server->proxyType() !== 'NONE') {
$this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n");
@@ -145,7 +153,7 @@ class General extends Component
$this->application->settings->save();
$this->dispatch('success', 'Settings saved.');
$this->application->refresh();
if ($this->ports_exposes !== $this->application->ports_exposes) {
if ($this->ports_exposes !== $this->application->ports_exposes || $this->is_container_label_escape_enabled !== $this->application->settings->is_container_label_escape_enabled) {
$this->resetDefaultLabels(false);
}
}
@@ -156,6 +164,10 @@ class General extends Component
return;
}
['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation, 'initialDockerComposePrLocation' => $this->initialDockerComposePrLocation] = $this->application->loadComposeFile($isInit);
if (is_null($this->parsedServices)) {
$this->dispatch('error', "Failed to parse your docker-compose file. Please check the syntax and try again.");
return;
}
$compose = $this->application->parseCompose();
$services = data_get($compose, 'services');
if ($services) {
@@ -204,6 +216,9 @@ class General extends Component
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
$this->application->save();
$this->dispatch('success', 'Domain generated.');
if ($this->application->build_pack === 'dockercompose') {
$this->loadComposeFile();
}
return $domain;
}
public function updatedApplicationBaseDirectory()
@@ -257,9 +272,12 @@ class General extends Component
{
$this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n");
$this->ports_exposes = $this->application->ports_exposes;
$this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled;
$this->application->custom_labels = base64_encode($this->customLabels);
$this->application->save();
if ($this->application->build_pack === 'dockercompose') {
$this->loadComposeFile();
}
}
public function checkFqdns($showToaster = true)
@@ -300,11 +318,11 @@ class General extends Component
if ($this->application->build_pack === 'dockercompose' && $this->initialDockerComposeLocation !== $this->application->docker_compose_location) {
$compose_return = $this->loadComposeFile();
if ($compose_return instanceof \Livewire\Features\SupportEvents\Event) {
return;
return;
}
}
$this->validate();
if ($this->ports_exposes !== $this->application->ports_exposes) {
if ($this->ports_exposes !== $this->application->ports_exposes || $this->is_container_label_escape_enabled !== $this->application->settings->is_container_label_escape_enabled) {
$this->resetDefaultLabels();
}
if (data_get($this->application, 'build_pack') === 'dockerimage') {

View File

@@ -5,7 +5,7 @@ namespace App\Livewire\Project\Application;
use App\Actions\Application\StopApplication;
use App\Actions\Docker\GetContainersStatus;
use App\Events\ApplicationStatusChanged;
use App\Jobs\ContainerStatusJob;
use App\Jobs\ContainerStatusJob;
use App\Jobs\ServerStatusJob;
use App\Models\Application;
use Livewire\Component;
@@ -14,6 +14,8 @@ use Visus\Cuid2\Cuid2;
class Heading extends Component
{
public Application $application;
public ?string $lastDeploymentInfo = null;
public ?string $lastDeploymentLink = null;
public array $parameters;
protected string $deploymentUuid;
@@ -28,6 +30,9 @@ class Heading extends Component
public function mount()
{
$this->parameters = get_route_parameters();
$lastDeployment = $this->application->get_last_successful_deployment();
$this->lastDeploymentInfo = data_get_str($lastDeployment, 'commit')->limit(7) . ' ' . data_get($lastDeployment, 'commit_message');
$this->lastDeploymentLink = $this->application->gitCommitLink(data_get($lastDeployment, 'commit'));
}
public function check_status($showNotification = false)

View File

@@ -35,7 +35,7 @@ class BackupEdit extends Component
public function mount()
{
$this->parameters = get_route_parameters();
if (is_null($this->backup->s3_storage_id)) {
if (is_null(data_get($this->backup, 's3_storage_id'))) {
$this->backup->s3_storage_id = 'default';
}
}

View File

@@ -20,7 +20,7 @@ class BackupExecutions extends Component
public function cleanupFailed()
{
$this->backup->executions()->where('status', 'failed')->delete();
$this->backup?->executions()->where('status', 'failed')->delete();
$this->refreshBackupExecutions();
}
public function deleteBackup($exeuctionId)

View File

@@ -12,7 +12,6 @@ class Create extends Component
public $type;
public function mount()
{
$services = getServiceTemplates();
$type = str(request()->query('type'));
$destination_uuid = request()->query('destination');
$server_id = request()->query('server_id');
@@ -25,83 +24,87 @@ class Create extends Component
if (!$environment) {
return redirect()->route('dashboard');
}
if (in_array($type, DATABASE_TYPES)) {
if ($type->value() === "postgresql") {
$database = create_standalone_postgresql($environment->id, $destination_uuid);
} else if ($type->value() === 'redis') {
$database = create_standalone_redis($environment->id, $destination_uuid);
} else if ($type->value() === 'mongodb') {
$database = create_standalone_mongodb($environment->id, $destination_uuid);
} else if ($type->value() === 'mysql') {
$database = create_standalone_mysql($environment->id, $destination_uuid);
} else if ($type->value() === 'mariadb') {
$database = create_standalone_mariadb($environment->id, $destination_uuid);
} else if ($type->value() === 'keydb') {
$database = create_standalone_keydb($environment->id, $destination_uuid);
} else if ($type->value() === 'dragonfly') {
$database = create_standalone_dragonfly($environment->id, $destination_uuid);
} else if ($type->value() === 'clickhouse') {
$database = create_standalone_clickhouse($environment->id, $destination_uuid);
}
return redirect()->route('project.database.configuration', [
'project_uuid' => $project->uuid,
'environment_name' => $environment->name,
'database_uuid' => $database->uuid,
]);
}
if ($type->startsWith('one-click-service-') && !is_null((int)$server_id)) {
$oneClickServiceName = $type->after('one-click-service-')->value();
$oneClickService = data_get($services, "$oneClickServiceName.compose");
$oneClickDotEnvs = data_get($services, "$oneClickServiceName.envs", null);
if ($oneClickDotEnvs) {
$oneClickDotEnvs = str(base64_decode($oneClickDotEnvs))->split('/\r\n|\r|\n/')->filter(function ($value) {
return !empty($value);
});
}
if ($oneClickService) {
$destination = StandaloneDocker::whereUuid($destination_uuid)->first();
$service_payload = [
'name' => "$oneClickServiceName-" . str()->random(10),
'docker_compose_raw' => base64_decode($oneClickService),
'environment_id' => $environment->id,
'service_type' => $oneClickServiceName,
'server_id' => (int) $server_id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
];
if ($oneClickServiceName === 'cloudflared') {
data_set($service_payload, 'connect_to_docker_network', true);
if (isset($type) && isset($destination_uuid) && isset($server_id)) {
$services = getServiceTemplates();
if (in_array($type, DATABASE_TYPES)) {
if ($type->value() === "postgresql") {
$database = create_standalone_postgresql($environment->id, $destination_uuid);
} else if ($type->value() === 'redis') {
$database = create_standalone_redis($environment->id, $destination_uuid);
} else if ($type->value() === 'mongodb') {
$database = create_standalone_mongodb($environment->id, $destination_uuid);
} else if ($type->value() === 'mysql') {
$database = create_standalone_mysql($environment->id, $destination_uuid);
} else if ($type->value() === 'mariadb') {
$database = create_standalone_mariadb($environment->id, $destination_uuid);
} else if ($type->value() === 'keydb') {
$database = create_standalone_keydb($environment->id, $destination_uuid);
} else if ($type->value() === 'dragonfly') {
$database = create_standalone_dragonfly($environment->id, $destination_uuid);
} else if ($type->value() === 'clickhouse') {
$database = create_standalone_clickhouse($environment->id, $destination_uuid);
}
$service = Service::create($service_payload);
$service->name = "$oneClickServiceName-" . $service->uuid;
$service->save();
if ($oneClickDotEnvs?->count() > 0) {
$oneClickDotEnvs->each(function ($value) use ($service) {
$key = str()->before($value, '=');
$value = str(str()->after($value, '='));
$generatedValue = $value;
if ($value->contains('SERVICE_')) {
$command = $value->after('SERVICE_')->beforeLast('_');
$generatedValue = generateEnvValue($command->value(), $service);
}
EnvironmentVariable::create([
'key' => $key,
'value' => $generatedValue,
'service_id' => $service->id,
'is_build_time' => false,
'is_preview' => false,
]);
});
}
$service->parse(isNew: true);
return redirect()->route('project.service.configuration', [
'service_uuid' => $service->uuid,
'environment_name' => $environment->name,
return redirect()->route('project.database.configuration', [
'project_uuid' => $project->uuid,
'environment_name' => $environment->name,
'database_uuid' => $database->uuid,
]);
}
if ($type->startsWith('one-click-service-') && !is_null((int)$server_id)) {
$oneClickServiceName = $type->after('one-click-service-')->value();
$oneClickService = data_get($services, "$oneClickServiceName.compose");
$oneClickDotEnvs = data_get($services, "$oneClickServiceName.envs", null);
if ($oneClickDotEnvs) {
$oneClickDotEnvs = str(base64_decode($oneClickDotEnvs))->split('/\r\n|\r|\n/')->filter(function ($value) {
return !empty($value);
});
}
if ($oneClickService) {
$destination = StandaloneDocker::whereUuid($destination_uuid)->first();
$service_payload = [
'name' => "$oneClickServiceName-" . str()->random(10),
'docker_compose_raw' => base64_decode($oneClickService),
'environment_id' => $environment->id,
'service_type' => $oneClickServiceName,
'server_id' => (int) $server_id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
];
if ($oneClickServiceName === 'cloudflared') {
data_set($service_payload, 'connect_to_docker_network', true);
}
$service = Service::create($service_payload);
$service->name = "$oneClickServiceName-" . $service->uuid;
$service->save();
if ($oneClickDotEnvs?->count() > 0) {
$oneClickDotEnvs->each(function ($value) use ($service) {
$key = str()->before($value, '=');
$value = str(str()->after($value, '='));
$generatedValue = $value;
if ($value->contains('SERVICE_')) {
$command = $value->after('SERVICE_')->beforeLast('_');
$generatedValue = generateEnvValue($command->value(), $service);
}
EnvironmentVariable::create([
'key' => $key,
'value' => $generatedValue,
'service_id' => $service->id,
'is_build_time' => false,
'is_preview' => false,
]);
});
}
$service->parse(isNew: true);
return redirect()->route('project.service.configuration', [
'service_uuid' => $service->uuid,
'environment_name' => $environment->name,
'project_uuid' => $project->uuid,
]);
}
}
$this->type = $type->value();
}
$this->type = $type->value();
}
public function render()
{

View File

@@ -3,7 +3,6 @@
namespace App\Livewire\Project\Service;
use App\Actions\Docker\GetContainersStatus;
use App\Jobs\ContainerStatusJob;
use App\Models\Service;
use Livewire\Component;

View File

@@ -12,6 +12,7 @@ class EditCompose extends Component
protected $rules = [
'service.docker_compose_raw' => 'required',
'service.docker_compose' => 'required',
'service.is_container_label_escape_enabled' => 'required',
];
public function mount()
{
@@ -23,6 +24,14 @@ class EditCompose extends Component
$this->dispatch('info', "Saving new docker compose...");
$this->dispatch('saveCompose', $this->service->docker_compose_raw);
}
public function instantSave()
{
$this->validate([
'service.is_container_label_escape_enabled' => 'required',
]);
$this->service->save(['is_container_label_escape_enabled' => $this->service->is_container_label_escape_enabled]);
$this->dispatch('success', "Service updated successfully");
}
public function render()
{
return view('livewire.project.service.edit-compose');

View File

@@ -5,11 +5,11 @@ namespace App\Livewire\Project\Shared\EnvironmentVariable;
use App\Models\EnvironmentVariable;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
use Illuminate\Support\Str;
class All extends Component
{
public $resource;
public string $resourceClass;
public bool $showPreview = false;
public ?string $modalId = null;
public ?string $variables = null;
@@ -19,17 +19,44 @@ class All extends Component
'refreshEnvs',
'saveKey' => 'submit',
];
protected $rules = [
'resource.settings.is_env_sorting_enabled' => 'required|boolean',
];
public function mount()
{
$resourceClass = get_class($this->resource);
$this->resourceClass = get_class($this->resource);
$resourceWithPreviews = ['App\Models\Application'];
$simpleDockerfile = !is_null(data_get($this->resource, 'dockerfile'));
if (Str::of($resourceClass)->contains($resourceWithPreviews) && !$simpleDockerfile) {
if (str($this->resourceClass)->contains($resourceWithPreviews) && !$simpleDockerfile) {
$this->showPreview = true;
}
$this->modalId = new Cuid2(7);
$this->sortMe();
$this->getDevView();
}
public function sortMe()
{
if ($this->resourceClass === 'App\Models\Application' && data_get($this->resource, 'build_pack') !== 'dockercompose') {
if ($this->resource->settings->is_env_sorting_enabled) {
$this->resource->environment_variables = $this->resource->environment_variables->sortBy('key');
$this->resource->environment_variables_preview = $this->resource->environment_variables_preview->sortBy('key');
} else {
$this->resource->environment_variables = $this->resource->environment_variables->sortBy('id');
$this->resource->environment_variables_preview = $this->resource->environment_variables_preview->sortBy('id');
}
}
$this->getDevView();
}
public function instantSave()
{
if ($this->resourceClass === 'App\Models\Application' && data_get($this->resource, 'build_pack') !== 'dockercompose') {
$this->resource->settings->save();
$this->dispatch('success', 'Environment variable settings updated.');
$this->sortMe();
}
}
public function getDevView()
{
$this->variables = $this->resource->environment_variables->map(function ($item) {
@@ -40,7 +67,7 @@ class All extends Component
return "$item->key=(multiline, edit in normal view)";
}
return "$item->key=$item->value";
})->sort()->join('
})->join('
');
if ($this->showPreview) {
$this->variablesPreview = $this->resource->environment_variables_preview->map(function ($item) {
@@ -51,13 +78,18 @@ class All extends Component
return "$item->key=(multiline, edit in normal view)";
}
return "$item->key=$item->value";
})->sort()->join('
})->join('
');
}
}
public function switch()
{
$this->view = $this->view === 'normal' ? 'dev' : 'normal';
if ($this->view === 'normal') {
$this->view = 'dev';
} else {
$this->view = 'normal';
}
$this->sortMe();
}
public function saveVariables($isPreview)
{
@@ -66,6 +98,7 @@ class All extends Component
$this->resource->environment_variables_preview()->whereNotIn('key', array_keys($variables))->delete();
} else {
$variables = parseEnvFormatToArray($this->variables);
ray($variables, $this->variables);
$this->resource->environment_variables()->whereNotIn('key', array_keys($variables))->delete();
}
foreach ($variables as $key => $variable) {

View File

@@ -2,11 +2,14 @@
namespace App\Livewire\Project\Shared\ScheduledTask;
use Illuminate\Support\Collection;
use Livewire\Component;
class Add extends Component
{
public $parameters;
public string $type;
public Collection $containerNames;
public string $name;
public string $command;
public string $frequency;
@@ -29,6 +32,9 @@ class Add extends Component
public function mount()
{
$this->parameters = get_route_parameters();
if ($this->containerNames->count() > 0) {
$this->container = $this->containerNames->first();
}
}
public function submit()
@@ -40,6 +46,11 @@ class Add extends Component
$this->dispatch('error', 'Invalid Cron / Human expression.');
return;
}
if (empty($this->container) || $this->container == 'null') {
if ($this->type == 'service') {
$this->container = $this->subServiceName;
}
}
$this->dispatch('saveScheduledTask', [
'name' => $this->name,
'command' => $this->command,

View File

@@ -3,14 +3,13 @@
namespace App\Livewire\Project\Shared\ScheduledTask;
use App\Models\ScheduledTask;
use Illuminate\Support\Collection;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
use Illuminate\Support\Str;
class All extends Component
{
public $resource;
public string|null $modalId = null;
public Collection $containerNames;
public ?string $variables = null;
public array $parameters;
protected $listeners = ['refreshTasks', 'saveScheduledTask' => 'submit'];
@@ -18,7 +17,18 @@ class All extends Component
public function mount()
{
$this->parameters = get_route_parameters();
$this->modalId = new Cuid2(7);
if ($this->resource->type() == 'service') {
$this->containerNames = $this->resource->applications()->pluck('name');
$this->containerNames = $this->containerNames->merge($this->resource->databases()->pluck('name'));
} elseif ($this->resource->type() == 'application') {
if ($this->resource->build_pack === 'dockercompose') {
$parsed = $this->resource->parseCompose();
$containers = collect(data_get($parsed,'services'))->keys();
$this->containerNames = $containers;
} else {
$this->containerNames = collect([]);
}
}
}
public function refreshTasks()
{

View File

@@ -17,6 +17,7 @@ class Show extends Component
public string $type;
protected $rules = [
'task.enabled' => 'required|boolean',
'task.name' => 'required|string',
'task.command' => 'required|string',
'task.frequency' => 'required|string',
@@ -45,9 +46,18 @@ class Show extends Component
$this->task = ModelsScheduledTask::where('uuid', request()->route('task_uuid'))->first();
}
public function instantSave()
{
$this->validateOnly('task.enabled');
$this->task->save(['enabled' => $this->task->enabled]);
$this->dispatch('success', 'Scheduled task updated.');
$this->dispatch('refreshTasks');
}
public function submit()
{
$this->validate();
$this->task->name = str($this->task->name)->trim()->value();
$this->task->container = str($this->task->container)->trim()->value();
$this->task->save();
$this->dispatch('success', 'Scheduled task updated.');
$this->dispatch('refreshTasks');
@@ -60,11 +70,9 @@ class Show extends Component
if ($this->type == 'application') {
return redirect()->route('project.application.configuration', $this->parameters);
}
else {
} else {
return redirect()->route('project.service.configuration', $this->parameters);
}
} catch (\Exception $e) {
return handleError($e);
}

View File

@@ -13,7 +13,7 @@ class Configuration extends Component
public bool $is_auto_update_enabled;
public bool $is_registration_enabled;
public bool $is_dns_validation_enabled;
public bool $next_channel;
// public bool $next_channel;
protected string $dynamic_config_path = '/data/coolify/proxy/dynamic';
protected Server $server;
@@ -37,7 +37,7 @@ class Configuration extends Component
$this->do_not_track = $this->settings->do_not_track;
$this->is_auto_update_enabled = $this->settings->is_auto_update_enabled;
$this->is_registration_enabled = $this->settings->is_registration_enabled;
$this->next_channel = $this->settings->next_channel;
// $this->next_channel = $this->settings->next_channel;
$this->is_dns_validation_enabled = $this->settings->is_dns_validation_enabled;
}
@@ -47,12 +47,12 @@ class Configuration extends Component
$this->settings->is_auto_update_enabled = $this->is_auto_update_enabled;
$this->settings->is_registration_enabled = $this->is_registration_enabled;
$this->settings->is_dns_validation_enabled = $this->is_dns_validation_enabled;
if ($this->next_channel) {
$this->settings->next_channel = false;
$this->next_channel = false;
} else {
$this->settings->next_channel = $this->next_channel;
}
// if ($this->next_channel) {
// $this->settings->next_channel = false;
// $this->next_channel = false;
// } else {
// $this->settings->next_channel = $this->next_channel;
// }
$this->settings->save();
$this->dispatch('success', 'Settings updated!');
}

View File

@@ -0,0 +1,117 @@
<?php
namespace App\Livewire\Team;
use App\Models\Team;
use App\Models\User;
use Livewire\Component;
class AdminView extends Component
{
public $users;
public ?string $search = "";
public function mount()
{
if (!isInstanceAdmin()) {
return redirect()->route('dashboard');
}
$this->getUsers();
}
public function submitSearch()
{
if ($this->search !== "") {
$this->users = User::where(function ($query) {
$query->where('name', 'like', "%{$this->search}%")
->orWhere('email', 'like', "%{$this->search}%");
})->get()->filter(function ($user) {
return $user->id !== auth()->id();
});
} else {
$this->getUsers();
}
}
public function getUsers()
{
$this->users = User::where('id', '!=', auth()->id())->get();
// $this->users = User::all();
}
private function finalizeDeletion(User $user, Team $team)
{
$servers = $team->servers;
foreach ($servers as $server) {
$resources = $server->definedResources();
foreach ($resources as $resource) {
ray("Deleting resource: " . $resource->name);
$resource->forceDelete();
}
ray("Deleting server: " . $server->name);
$server->forceDelete();
}
$projects = $team->projects;
foreach ($projects as $project) {
ray("Deleting project: " . $project->name);
$project->forceDelete();
}
$team->members()->detach($user->id);
ray('Deleting team: ' . $team->name);
$team->delete();
}
public function delete($id)
{
$user = User::find($id);
$teams = $user->teams;
foreach ($teams as $team) {
ray($team->name);
$user_alone_in_team = $team->members->count() === 1;
if ($team->id === 0) {
if ($user_alone_in_team) {
ray('user is alone in the root team, do nothing');
return $this->dispatch('error', 'User is alone in the root team, cannot delete');
}
}
if ($user_alone_in_team) {
ray('user is alone in the team');
$this->finalizeDeletion($user, $team);
continue;
}
ray('user is not alone in the team');
if ($user->isOwner()) {
$found_other_owner_or_admin = $team->members->filter(function ($member) {
return $member->pivot->role === 'owner' || $member->pivot->role === 'admin';
})->where('id', '!=', $user->id)->first();
if ($found_other_owner_or_admin) {
ray('found other owner or admin');
$team->members()->detach($user->id);
continue;
} else {
$found_other_member_who_is_not_owner = $team->members->filter(function ($member) {
return $member->pivot->role === 'member';
})->first();
if ($found_other_member_who_is_not_owner) {
ray('found other member who is not owner');
$found_other_member_who_is_not_owner->pivot->role = 'owner';
$found_other_member_who_is_not_owner->pivot->save();
$team->members()->detach($user->id);
} else {
// This should never happen as if the user is the only member in the team, the team should be deleted already.
ray('found no other member who is not owner');
$this->finalizeDeletion($user, $team);
}
continue;
}
} else {
ray('user is not owner');
$team->members()->detach($user->id);
}
}
ray("Deleting user: " . $user->name);
$user->delete();
$this->getUsers();
}
public function render()
{
return view('livewire.team.admin-view');
}
}

View File

@@ -3,7 +3,7 @@
namespace App\Livewire;
use App\Actions\Server\UpdateCoolify;
use App\Models\InstanceSettings;
use Livewire\Component;
use DanHarrin\LivewireRateLimiting\WithRateLimiting;
@@ -11,6 +11,7 @@ class Upgrade extends Component
{
use WithRateLimiting;
public bool $showProgress = false;
public bool $updateInProgress = false;
public bool $isUpgradeAvailable = false;
public string $latestVersion = '';
@@ -22,23 +23,17 @@ class Upgrade extends Component
if (isDev()) {
$this->isUpgradeAvailable = true;
}
$settings = InstanceSettings::get();
if ($settings->next_channel) {
$this->isUpgradeAvailable = true;
$this->latestVersion = 'next';
}
}
public function upgrade()
{
try {
if ($this->showProgress) {
if ($this->updateInProgress) {
return;
}
$this->rateLimit(1, 30);
$this->showProgress = true;
$this->rateLimit(1, 60);
$this->updateInProgress = true;
UpdateCoolify::run(force: true, async: true);
$this->dispatch('success', "Updating Coolify to {$this->latestVersion} version...");
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -113,6 +113,18 @@ class Application extends BaseModel
}
return null;
}
public function failedTaskLink($task_uuid)
{
if (data_get($this, 'environment.project.uuid')) {
return route('project.application.scheduled-tasks', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'application_uuid' => data_get($this, 'uuid'),
'task_uuid' => $task_uuid
]);
}
return null;
}
public function settings()
{
return $this->hasOne(ApplicationSetting::class);
@@ -146,9 +158,13 @@ class Application extends BaseModel
if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) {
return "{$this->source->html_url}/{$this->git_repository}/tree/{$this->git_branch}";
}
// Convert the SSH URL to HTTPS URL
if (strpos($this->git_repository, 'git@') === 0) {
$git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository);
return "https://{$git_repository}/tree/{$this->git_branch}";
}
return $this->git_repository;
}
);
}
@@ -159,6 +175,11 @@ class Application extends BaseModel
if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) {
return "{$this->source->html_url}/{$this->git_repository}/settings/hooks";
}
// Convert the SSH URL to HTTPS URL
if (strpos($this->git_repository, 'git@') === 0) {
$git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository);
return "https://{$git_repository}/settings/hooks";
}
return $this->git_repository;
}
);
@@ -171,10 +192,29 @@ class Application extends BaseModel
if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) {
return "{$this->source->html_url}/{$this->git_repository}/commits/{$this->git_branch}";
}
// Convert the SSH URL to HTTPS URL
if (strpos($this->git_repository, 'git@') === 0) {
$git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository);
return "https://{$git_repository}/commits/{$this->git_branch}";
}
return $this->git_repository;
}
);
}
public function gitCommitLink($link): string
{
if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) {
if (str($this->source->html_url)->contains('bitbucket')) {
return "{$this->source->html_url}/{$this->git_repository}/commits/{$link}";
}
return "{$this->source->html_url}/{$this->git_repository}/commit/{$link}";
}
if (strpos($this->git_repository, 'git@') === 0) {
$git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository);
return "https://{$git_repository}/commit/{$link}";
}
return $this->git_repository;
}
public function dockerfileLocation(): Attribute
{
return Attribute::make(
@@ -429,6 +469,10 @@ class Application extends BaseModel
}
return false;
}
public function get_last_successful_deployment()
{
return ApplicationDeploymentQueue::where('application_id', $this->id)->where('status', 'finished')->where('pull_request_id', 0)->orderBy('created_at', 'desc')->first();
}
public function get_last_days_deployments()
{
return ApplicationDeploymentQueue::where('application_id', $this->id)->where('created_at', '>=', now()->subDays(7))->orderBy('created_at', 'desc')->get();
@@ -847,7 +891,7 @@ class Application extends BaseModel
if (!$composeFileContent) {
$this->docker_compose_location = $initialDockerComposeLocation;
$this->save();
throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile");
throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile<br><br>Check if you used the right extension (.yaml or .yml) in the compose file name.");
} else {
$this->docker_compose_raw = $composeFileContent;
$this->save();
@@ -964,7 +1008,8 @@ class Application extends BaseModel
getFilesystemVolumesFromServer($this, $isInit);
}
public function parseHealthcheckFromDockerfile($dockerfile, bool $isInit = false) {
public function parseHealthcheckFromDockerfile($dockerfile, bool $isInit = false)
{
if (str($dockerfile)->contains('HEALTHCHECK') && ($this->isHealthcheckDisabled() || $isInit)) {
$healthcheckCommand = null;
$lines = $dockerfile->toArray();

View File

@@ -9,7 +9,8 @@ class ApplicationDeploymentQueue extends Model
{
protected $guarded = [];
public function setStatus(string $status) {
public function setStatus(string $status)
{
$this->update([
'status' => $status,
]);
@@ -21,7 +22,13 @@ class ApplicationDeploymentQueue extends Model
}
return collect(json_decode($this->logs))->where('name', $name)->first()?->output ?? null;
}
public function commitMessage()
{
if (empty($this->commit_message) || is_null($this->commit_message)) {
return null;
}
return str($this->commit_message)->trim()->limit(50)->value();
}
public function addLogEntry(string $message, string $type = 'stdout', bool $hidden = false)
{
if ($type === 'error') {

View File

@@ -8,6 +8,18 @@ use Illuminate\Database\Eloquent\Model;
class Environment extends Model
{
protected $guarded = [];
protected static function booted()
{
static::deleting(function ($environment) {
$shared_variables = $environment->environment_variables();
foreach ($shared_variables as $shared_variable) {
ray('Deleting environment shared variable: ' . $shared_variable->name);
$shared_variable->delete();
}
});
}
public function isEmpty()
{
return $this->applications()->count() == 0 &&

View File

@@ -25,6 +25,11 @@ class Project extends BaseModel
static::deleting(function ($project) {
$project->environments()->delete();
$project->settings()->delete();
$shared_variables = $project->environment_variables();
foreach ($shared_variables as $shared_variable) {
ray('Deleting project shared variable: ' . $shared_variable->name);
$shared_variable->delete();
}
});
}
public function environment_variables()
@@ -55,6 +60,7 @@ class Project extends BaseModel
return $this->hasManyThrough(Application::class, Environment::class);
}
public function postgresqls()
{
return $this->hasManyThrough(StandalonePostgresql::class, Environment::class);
@@ -91,4 +97,7 @@ class Project extends BaseModel
{
return $this->applications()->count() + $this->postgresqls()->count() + $this->redis()->count() + $this->mongodbs()->count() + $this->mysqls()->count() + $this->mariadbs()->count() + $this->keydbs()->count() + $this->dragonflies()->count() + $this->services()->count() + $this->clickhouses()->count();
}
public function databases() {
return $this->postgresqls()->get()->merge($this->redis()->get())->merge($this->mongodbs()->get())->merge($this->mysqls()->get())->merge($this->mariadbs()->get())->merge($this->keydbs()->get())->merge($this->dragonflies()->get())->merge($this->clickhouses()->get());
}
}

View File

@@ -450,14 +450,16 @@ class Service extends BaseModel
$data = collect([]);
$admin_user = $this->environment_variables()->where('key', 'SERVICE_USER_ADMIN')->first();
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ADMIN')->first();
$data = $data->merge([
'User' => [
'key' => 'SERVICE_USER_ADMIN',
'value' => data_get($admin_user, 'value', 'admin'),
'readonly' => true,
'rules' => 'required',
],
]);
if ($admin_user) {
$data = $data->merge([
'User' => [
'key' => 'SERVICE_USER_ADMIN',
'value' => data_get($admin_user, 'value', 'admin'),
'readonly' => true,
'rules' => 'required',
],
]);
}
if ($admin_password) {
$data = $data->merge([
'Password' => [
@@ -651,6 +653,18 @@ class Service extends BaseModel
}
return null;
}
public function failedTaskLink($task_uuid)
{
if (data_get($this, 'environment.project.uuid')) {
return route('project.service.scheduled-tasks', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'application_uuid' => data_get($this, 'uuid'),
'task_uuid' => $task_uuid
]);
}
return null;
}
public function documentation()
{
$services = getServiceTemplates();
@@ -677,6 +691,17 @@ class Service extends BaseModel
{
return $this->belongsTo(Server::class);
}
public function byUuid(string $uuid) {
$app = $this->applications()->whereUuid($uuid)->first();
if ($app) {
return $app;
}
$db = $this->databases()->whereUuid($uuid)->first();
if ($db) {
return $db;
}
return null;
}
public function byName(string $name)
{
$app = $this->applications()->whereName($name)->first();

View File

@@ -26,6 +26,34 @@ class Team extends Model implements SendsDiscord, SendsEmail
throw new \Exception('You are not allowed to update this team.');
}
});
static::deleting(function ($team) {
$keys = $team->privateKeys;
foreach ($keys as $key) {
ray('Deleting key: ' . $key->name);
$key->delete();
}
$sources = $team->sources();
foreach ($sources as $source) {
ray('Deleting source: ' . $source->name);
$source->delete();
}
$tags = Tag::whereTeamId($team->id)->get();
foreach ($tags as $tag) {
ray('Deleting tag: ' . $tag->name);
$tag->delete();
}
$shared_variables = $team->environment_variables();
foreach ($shared_variables as $shared_variable) {
ray('Deleting team shared variable: ' . $shared_variable->name);
$shared_variable->delete();
}
$s3s = $team->s3s;
foreach ($s3s as $s3) {
ray('Deleting s3: ' . $s3->name);
$s3->delete();
}
});
}
public function routeNotificationForDiscord()

View File

@@ -183,6 +183,7 @@ class User extends Authenticatable implements SendsEmail
if (data_get($this, 'pivot')) {
return $this->pivot->role;
}
return auth()->user()->teams->where('id', currentTeam()->id)->first()->pivot->role;
$user = auth()->user()->teams->where('id', currentTeam()->id)->first();
return data_get($user, 'pivot.role');
}
}

View File

@@ -43,7 +43,12 @@ class DeploymentSuccess extends Notification implements ShouldQueue
public function via(object $notifiable): array
{
return setNotificationChannels($notifiable, 'deployments');
$channels = setNotificationChannels($notifiable, 'deployments');
if (isCloud()) {
// TODO: Make batch notifications work with email
$channels = array_diff($channels, ['App\Notifications\Channels\EmailChannel']);
}
return $channels;
}
public function toMail(): MailMessage
{

View File

@@ -14,22 +14,27 @@ class TelegramChannel
$buttons = data_get($data, 'buttons', []);
$telegramToken = data_get($telegramData, 'token');
$chatId = data_get($telegramData, 'chat_id');
$topicId = null;
$topicId = null;
$topicsInstance = get_class($notification);
switch ($topicsInstance) {
case 'App\Notifications\StatusChange':
$topicId = data_get($notifiable, 'telegram_notifications_status_changes_message_thread_id');
break;
case 'App\Notifications\Test':
$topicId = data_get($notifiable, 'telegram_notifications_test_message_thread_id');
break;
case 'App\Notifications\Deployment':
case 'App\Notifications\Application\StatusChanged':
$topicId = data_get($notifiable, 'telegram_notifications_status_changes_message_thread_id');
break;
case 'App\Notifications\Application\DeploymentSuccess':
case 'App\Notifications\Application\DeploymentFailed':
$topicId = data_get($notifiable, 'telegram_notifications_deployments_message_thread_id');
break;
case 'App\Notifications\DatabaseBackup':
case 'App\Notifications\Database\BackupSuccess':
case 'App\Notifications\Database\BackupFailed':
$topicId = data_get($notifiable, 'telegram_notifications_database_backups_message_thread_id');
break;
case 'App\Notifications\ScheduledTask\TaskFailed':
$topicId = data_get($notifiable, 'telegram_notifications_scheduled_tasks_thread_id');
break;
}
if (!$telegramToken || !$chatId || !$message) {
return;

View File

@@ -31,7 +31,7 @@ class ContainerRestarted extends Notification implements ShouldQueue
$mail->view('emails.container-restarted', [
'containerName' => $this->name,
'serverName' => $this->server->name,
'url' => $this->url ,
'url' => $this->url,
]);
return $mail;
}

View File

@@ -0,0 +1,64 @@
<?php
namespace App\Notifications\ScheduledTask;
use App\Models\ScheduledTask;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class TaskFailed extends Notification implements ShouldQueue
{
use Queueable;
public $backoff = 10;
public $tries = 2;
public ?string $url = null;
public function __construct(public ScheduledTask $task, public string $output)
{
if ($task->application) {
$this->url = $task->application->failedTaskLink($task->uuid);
} else if ($task->service) {
$this->url = $task->service->failedTaskLink($task->uuid);
}
}
public function via(object $notifiable): array
{
return setNotificationChannels($notifiable, 'scheduled_tasks');
}
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject("Coolify: [ACTION REQUIRED] Scheduled task ({$this->task->name}) failed.");
$mail->view('emails.scheduled-task-failed', [
'task' => $this->task,
'url' => $this->url,
'output' => $this->output,
]);
return $mail;
}
public function toDiscord(): string
{
return "Coolify: Scheduled task ({$this->task->name}, [link]({$this->url})) failed with output: {$this->output}";
}
public function toTelegram(): array
{
$message = "Coolify: Scheduled task ({$this->task->name}) failed with output: {$this->output}";
if ($this->url) {
$buttons[] = [
"text" => "Open task in Coolify",
"url" => (string) $this->url
];
}
return [
"message" => $message,
];
}
}

View File

@@ -6,7 +6,6 @@ use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Server;
use App\Models\StandaloneDocker;
use Illuminate\Support\Collection;
use Spatie\Url\Url;
function queue_application_deployment(Application $application, string $deployment_uuid, int | null $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false, ?string $git_type = null, bool $no_questions_asked = false, Server $server = null, StandaloneDocker $destination = null, bool $only_this_server = false, bool $rollback = false)

View File

@@ -95,6 +95,9 @@ function currentTeam()
function showBoarding(): bool
{
if (auth()->user()?->isMember()) {
return false;
}
return currentTeam()->show_boarding ?? false;
}
function refreshSession(?Team $team = null): void
@@ -1174,6 +1177,13 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
]
]);
}
if ($serviceLabels->count() > 0) {
if ($resource->is_container_label_escape_enabled) {
$serviceLabels = $serviceLabels->map(function ($value, $key) {
return escapeDollarSign($value);
});
}
}
data_set($service, 'labels', $serviceLabels->toArray());
data_forget($service, 'is_database');
if (!data_get($service, 'restart')) {
@@ -1225,13 +1235,13 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
try {
$yaml = Yaml::parse($resource->docker_compose_pr_raw);
} catch (\Exception $e) {
throw new \Exception($e->getMessage());
return;
}
} else {
try {
$yaml = Yaml::parse($resource->docker_compose_raw);
} catch (\Exception $e) {
throw new \Exception($e->getMessage());
return;
}
}
$server = $resource->destination->server;
@@ -1259,6 +1269,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$servicePorts = collect(data_get($service, 'ports', []));
$serviceNetworks = collect(data_get($service, 'networks', []));
$serviceVariables = collect(data_get($service, 'environment', []));
$serviceDependencies = collect(data_get($service, 'depends_on', []));
$serviceLabels = collect(data_get($service, 'labels', []));
$serviceBuildVariables = collect(data_get($service, 'build.args', []));
$serviceVariables = $serviceVariables->merge($serviceBuildVariables);
@@ -1275,11 +1286,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$serviceLabels->push("$removedLabelName=$removedLabel");
}
}
if ($serviceLabels->count() > 0) {
$serviceLabels = $serviceLabels->map(function ($value, $key) {
return escapeDollarSign($value);
});
}
$baseName = generateApplicationContainerName($resource, $pull_request_id);
$containerName = "$serviceName-$baseName";
if (count($serviceVolumes) > 0) {
@@ -1370,6 +1377,13 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
data_set($service, 'volumes', $serviceVolumes->toArray());
}
if ($pull_request_id !== 0 && count($serviceDependencies) > 0) {
$serviceDependencies = $serviceDependencies->map(function ($dependency) use ($pull_request_id) {
return $dependency . "-pr-$pull_request_id";
});
data_set($service, 'depends_on', $serviceDependencies->toArray());
}
// Decide if the service is a database
$isDatabase = isDatabaseImage(data_get_str($service, 'image'));
data_set($service, 'is_database', $isDatabase);
@@ -1652,6 +1666,13 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
]
]);
}
if ($serviceLabels->count() > 0) {
if ($resource->settings->is_container_label_escape_enabled) {
$serviceLabels = $serviceLabels->map(function ($value, $key) {
return escapeDollarSign($value);
});
}
}
data_set($service, 'labels', $serviceLabels->toArray());
data_forget($service, 'is_database');
if (!data_get($service, 'restart')) {

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,34 @@
<?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->boolean('telegram_notifications_scheduled_tasks')->default(true);
$table->boolean('smtp_notifications_scheduled_tasks')->default(false)->after('smtp_notifications_status_changes');
$table->boolean('discord_notifications_scheduled_tasks')->default(true)->after('discord_notifications_status_changes');
$table->text('telegram_notifications_scheduled_tasks_thread_id')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('teams', function (Blueprint $table) {
$table->dropColumn('telegram_notifications_scheduled_tasks');
$table->dropColumn('smtp_notifications_scheduled_tasks');
$table->dropColumn('discord_notifications_scheduled_tasks');
$table->dropColumn('telegram_notifications_scheduled_tasks_thread_id');
});
}
};

View File

@@ -0,0 +1,42 @@
<?php
namespace Database\Seeders;
use App\Models\Team;
use App\Models\User;
use Illuminate\Database\Seeder;
class TestTeamSeeder extends Seeder
{
public function run(): void
{
// User has 2 teams, 1 personal, 1 other where it is the owner and no other members are in the team
$user = User::factory()->create([
'name' => '1 personal, 1 other team, owner, no other members',
'email' => '1@example.com',
]);
$team = Team::create([
'name' => "1@example.com",
'personal_team' => false,
'show_boarding' => true
]);
$user->teams()->attach($team, ['role' => 'owner']);
// User has 2 teams, 1 personal, 1 other where it is the owner and 1 other member is in the team
$user = User::factory()->create([
'name' => 'owner: 1 personal, 1 other team, owner, 1 other member',
'email' => '2@example.com',
]);
$team = Team::create([
'name' => "2@example.com",
'personal_team' => false,
'show_boarding' => true
]);
$user->teams()->attach($team, ['role' => 'owner']);
$user = User::factory()->create([
'name' => 'member: 1 personal, 1 other team, owner, 1 other member',
'email' => '3@example.com',
]);
$team->members()->attach($user, ['role' => 'member']);
}
}

View File

@@ -2,15 +2,15 @@ FROM alpine:3.17
ARG TARGETPLATFORM
# https://download.docker.com/linux/static/stable/
ARG DOCKER_VERSION=24.0.9
ARG DOCKER_VERSION=26.1.2
# https://github.com/docker/compose/releases
ARG DOCKER_COMPOSE_VERSION=2.25.0
ARG DOCKER_COMPOSE_VERSION=2.27.0
# https://github.com/docker/buildx/releases
ARG DOCKER_BUILDX_VERSION=0.13.1
ARG DOCKER_BUILDX_VERSION=0.14.0
# https://github.com/buildpacks/pack/releases
ARG PACK_VERSION=0.33.2
# https://github.com/railwayapp/nixpacks/releases
ARG NIXPACKS_VERSION=1.21.2
ARG NIXPACKS_VERSION=1.21.3
USER root
WORKDIR /artifacts

View File

@@ -2,7 +2,7 @@ FROM serversideup/php:8.2-fpm-nginx-v2.2.1
ARG TARGETPLATFORM
# https://github.com/cloudflare/cloudflared/releases
ARG CLOUDFLARED_VERSION=2024.2.1
ARG CLOUDFLARED_VERSION=2024.4.1
ARG POSTGRES_VERSION=15
RUN apt-get update

View File

@@ -15,7 +15,7 @@ FROM serversideup/php:8.2-fpm-nginx-v2.2.1
ARG TARGETPLATFORM
# https://github.com/cloudflare/cloudflared/releases
ARG CLOUDFLARED_VERSION=2024.2.1
ARG CLOUDFLARED_VERSION=2024.4.1
ARG POSTGRES_VERSION=15
WORKDIR /var/www/html

View File

@@ -2,11 +2,11 @@ FROM debian:12-slim
ARG TARGETPLATFORM
# https://download.docker.com/linux/static/stable/
ARG DOCKER_VERSION=24.0.5
ARG DOCKER_VERSION=26.1.2
# https://github.com/docker/compose/releases
ARG DOCKER_COMPOSE_VERSION=2.21.0
ARG DOCKER_COMPOSE_VERSION=2.27.0
# https://github.com/docker/buildx/releases
ARG DOCKER_BUILDX_VERSION=0.11.2
ARG DOCKER_BUILDX_VERSION=0.14.0
USER root
WORKDIR /root

1
public/svgs/twenty.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="none" height="136" viewBox="0 0 136 136" width="136" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><clipPath id="a"><rect height="136" rx="16" width="136"/></clipPath><g clip-path="url(#a)"><path d="m136 .00002289h-136l.00014448 135.99997711h135.99985552zm-108.73 50.64007711c0-7.43 6.03-13.46 13.46-13.46h25.91c.38 0 .73.23.89.58s.09.76-.17 1.05l-5.68 6.17c-.99 1.07-2.38 1.69-3.84 1.69h-17.04c-2.23 0-4.04 1.81-4.04 4.04v10.18c0 1.31-1.06 2.37-2.37 2.37h-4.74c-1.31 0-2.37-1.06-2.37-2.37v-10.25zm80.61 34.72c0 7.43-6.03 13.4599-13.46 13.4599h-11.01c-7.43 0-13.46-6.0299-13.46-13.4599v-19.27c0-1.31.49-2.57 1.38-3.54l6.42-6.97c.27-.29.69-.39 1.07-.25.37.15.62.4999.62.8999v29.0701c0 2.23 1.81 4.04 4.04 4.04h10.88c2.23 0 4.04-1.81 4.04-4.04v-34.59c0-2.23-1.81-4.04-4.04-4.04h-12.65c-1.45 0-2.83.61-3.82 1.67l-37.73 41h22.67c1.31 0 2.37 1.06 2.37 2.37v4.74c0 1.31-1.06 2.3699-2.37 2.3699h-30.55c-2.77 0-5.02-2.2499-5.02-5.0199v-2.5101c0-1.26.47-2.4699 1.33-3.3999l42.3-45.95c2.8-3.04 6.73-4.76 10.86-4.76h12.66c7.43 0 13.46 6.03 13.46 13.46v34.72z" fill="#000"/><g fill="#fff"><path d="m27.27 50.6401c0-7.43 6.03-13.46 13.46-13.46h25.91c.38 0 .73.23.89.58s.09.76-.17 1.05l-5.68 6.17c-.99 1.07-2.38 1.69-3.84 1.69h-17.04c-2.23 0-4.04 1.81-4.04 4.04v10.18c0 1.31-1.06 2.37-2.37 2.37h-4.74c-1.31 0-2.37-1.06-2.37-2.37v-10.25z"/><path d="m107.88 85.3601c0 7.43-6.03 13.4599-13.46 13.4599h-11.01c-7.43 0-13.46-6.0299-13.46-13.4599v-19.27c0-1.31.49-2.57 1.38-3.54l6.42-6.97c.27-.29.69-.39 1.07-.25.37.15.62.4999.62.8999v29.0701c0 2.23 1.81 4.04 4.04 4.04h10.88c2.23 0 4.04-1.81 4.04-4.04v-34.59c0-2.23-1.81-4.04-4.04-4.04h-12.65c-1.45 0-2.83.61-3.82 1.67l-37.73 41h22.67c1.31 0 2.37 1.06 2.37 2.37v4.74c0 1.31-1.06 2.3699-2.37 2.3699h-30.55c-2.77 0-5.02-2.2499-5.02-5.0199v-2.5101c0-1.26.47-2.4699 1.33-3.3999l42.3-45.95c2.8-3.04 6.73-4.76 10.86-4.76h12.66c7.43 0 13.46 6.03 13.46 13.46v34.72z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -41,7 +41,7 @@ option {
}
.button {
@apply flex items-center justify-center gap-2 px-2 py-1 text-sm text-black normal-case border rounded cursor-pointer bg-neutral-200/50 border-neutral-300 hover:bg-neutral-300 dark:bg-coolgray-200 dark:text-white dark:hover:text-white dark:hover:bg-coolgray-500 dark:border-black hover:text-black disabled:cursor-not-allowed min-w-fit focus:outline-1 dark:disabled:text-neutral-600 disabled:border-none disabled:hover:bg-transparent disabled:bg-transparent disabled:text-neutral-300;
@apply flex items-center justify-center gap-2 px-2 py-1 text-sm text-black normal-case border rounded cursor-pointer bg-neutral-200/50 border-neutral-300 hover:bg-neutral-300 dark:bg-coolgray-200 dark:text-white dark:hover:text-white dark:hover:bg-coolgray-500 dark:border-coolgray-300 hover:text-black disabled:cursor-not-allowed min-w-fit focus:outline-1 dark:disabled:text-neutral-600 disabled:border-none disabled:hover:bg-transparent disabled:bg-transparent disabled:text-neutral-300;
}
button[isError]:not(:disabled) {

View File

@@ -161,10 +161,11 @@
class="{{ request()->is('storages*') ? 'menu-item-active menu-item' : 'menu-item' }}"
href="{{ route('storage.index') }}">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="M4 6a8 3 0 1 0 16 0A8 3 0 1 0 4 6"/>
<path d="M4 6v6a8 3 0 0 0 16 0V6"/>
<path d="M4 12v6a8 3 0 0 0 16 0v-6"/>
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path d="M4 6a8 3 0 1 0 16 0A8 3 0 1 0 4 6" />
<path d="M4 6v6a8 3 0 0 0 16 0V6" />
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
</g>
</svg>
S3 Storages
@@ -175,9 +176,11 @@
class="{{ request()->is('shared-variables*') ? 'menu-item-active menu-item' : 'menu-item' }}"
href="{{ route('shared-variables.index') }}">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="M5 4C2.5 9 2.5 14 5 20M19 4c2.5 5 2.5 10 0 16M9 9h1c1 0 1 1 2.016 3.527C13 15 13 16 14 16h1"/>
<path d="M8 16c1.5 0 3-2 4-3.5S14.5 9 16 9"/>
<g fill="none" stroke="currentColor" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2">
<path
d="M5 4C2.5 9 2.5 14 5 20M19 4c2.5 5 2.5 10 0 16M9 9h1c1 0 1 1 2.016 3.527C13 15 13 16 14 16h1" />
<path d="M8 16c1.5 0 3-2 4-3.5S14.5 9 16 9" />
</g>
</svg>
Shared Variables
@@ -318,9 +321,7 @@
<div class="flex-1"></div>
@if (isInstanceAdmin() && !isCloud())
<li>
@persist('upgrade')
<livewire:upgrade />
@endpersist
<livewire:upgrade />
</li>
@endif
<li>

View File

@@ -1,5 +1,10 @@
@props([
'lastDeploymentInfo' => null,
'lastDeploymentLink' => null,
'resource' => null,
])
<nav class="flex pt-2 pb-10">
<ol class="flex items-center flex-wrap gap-y-1">
<ol class="flex flex-wrap items-center gap-y-1">
<li class="inline-flex items-center">
<div class="flex items-center">
<a wire:navigate class="text-xs truncate lg:text-sm"
@@ -15,7 +20,6 @@
</li>
<li>
<div class="flex items-center">
<a class="text-xs truncate lg:text-sm"
href="{{ route('project.resource.index', ['environment_name' => $this->parameters['environment_name'], 'project_uuid' => $this->parameters['project_uuid']]) }}">{{ $this->parameters['environment_name'] }}</a>
<svg aria-hidden="true" class="w-4 h-4 mx-1 font-bold dark:text-warning" fill="currentColor"
@@ -40,7 +44,7 @@
@if ($resource->getMorphClass() == 'App\Models\Service')
<x-status.services :service="$resource" />
@else
<x-status.index :resource="$resource" />
<x-status.index :resource="$resource" :lastDeploymentInfo="$lastDeploymentInfo" :lastDeploymentLink="$lastDeploymentLink" />
@endif
</ol>
</nav>

View File

@@ -1,9 +1,14 @@
@props([
'lastDeploymentInfo' => null,
'lastDeploymentLink' => null,
'resource' => null,
])
@if (str($resource->status)->startsWith('running'))
<x-status.running :status="$resource->status" />
<x-status.running :status="$resource->status" :lastDeploymentInfo="$lastDeploymentInfo" :lastDeploymentLink="$lastDeploymentLink" />
@elseif(str($resource->status)->startsWith('restarting') ||
str($resource->status)->startsWith('starting') ||
str($resource->status)->startsWith('degraded'))
<x-status.restarting :status="$resource->status" />
<x-status.restarting :status="$resource->status" :lastDeploymentInfo="$lastDeploymentInfo" :lastDeploymentLink="$lastDeploymentLink" />
@else
<x-status.stopped :status="$resource->status" />
@endif

View File

@@ -1,12 +1,20 @@
@props([
'status' => 'Restarting',
'lastDeploymentInfo' => null,
'lastDeploymentLink' => null,
])
<div class="flex items-center">
<x-loading wire:loading.delay.longer />
<span wire:loading.remove.delay.longer class="flex items-center">
<div class="badge badge-warning "></div>
<div class="pl-2 pr-1 text-xs font-bold tracking-wider dark:text-warning">
{{ str($status)->before(':')->headline() }}
<div class="pl-2 pr-1 text-xs font-bold tracking-wider dark:text-warning" @if($lastDeploymentInfo) title="{{$lastDeploymentInfo}}" @endif>
@if ($lastDeploymentLink)
<a href="{{ $lastDeploymentLink }}" class="underline cursor-pointer">
{{ str($status)->before(':')->headline() }}
</a>
@else
{{ str($status)->before(':')->headline() }}
@endif
</div>
@if (!str($status)->startsWith('Proxy') && !str($status)->contains('('))
<div class="text-xs dark:text-warning">({{ str($status)->after(':') }})</div>

View File

@@ -1,12 +1,20 @@
@props([
'status' => 'Running',
'lastDeploymentInfo' => null,
'lastDeploymentLink' => null,
])
<div class="flex items-center">
<x-loading wire:loading.delay.longer />
<span wire:loading.remove.delay.longer class="flex items-center">
<div class="badge badge-success "></div>
<div class="pl-2 pr-1 text-xs font-bold tracking-wider text-success">
<div class="pl-2 pr-1 text-xs font-bold tracking-wider text-success" @if($lastDeploymentInfo) title="{{$lastDeploymentInfo}}" @endif>
@if ($lastDeploymentLink)
<a href="{{ $lastDeploymentLink }}" class="underline cursor-pointer">
{{ str($status)->before(':')->headline() }}
</a>
@else
{{ str($status)->before(':')->headline() }}
@endif
</div>
@if (!str($status)->startsWith('Proxy') && !str($status)->contains('('))
@if (str($status)->contains('unhealthy'))
@@ -17,8 +25,6 @@
</svg>
</x-slot:icon>
</x-helper>
{{-- @else
<div class="text-xs dark:text-success">({{ str($status)->after(':') }})</div> --}}
@endif
@endif
</span>

View File

@@ -15,6 +15,12 @@
href="{{ route('team.member.index') }}">
<button>Members</button>
</a>
@if (isInstanceAdmin())
<a class="{{ request()->routeIs('team.admin-view') ? 'dark:text-white' : '' }}"
href="{{ route('team.admin-view') }}">
<button>Admin View</button>
</a>
@endif
<div class="flex-1"></div>
</nav>
</div>

View File

@@ -11,22 +11,22 @@
<div class="grid gap-2 lg:grid-cols-1">
@forelse ($destinations as $destination)
@if ($destination->getMorphClass() === 'App\Models\StandaloneDocker')
<div class="box group">
<a class="flex flex-col mx-6"
<a class="box group"
href="{{ route('destination.show', ['destination_uuid' => data_get($destination, 'uuid')]) }}">
<div class="box-title">{{ $destination->name }}</div>
<div class="box-description">server: {{ $destination->server->name }}</div>
<div class="flex flex-col mx-6">
<div class="box-title">{{ $destination->name }}</div>
<div class="box-description">server: {{ $destination->server->name }}</div>
</div>
</a>
</div>
@endif
@if ($destination->getMorphClass() === 'App\Models\SwarmDocker')
<div class="box group">
<a class="flex flex-col mx-6"
<a class="box group"
href="{{ route('destination.show', ['destination_uuid' => data_get($destination, 'uuid')]) }}">
<div class="box-title">{{ $destination->name }}</div>
<div class="box-description">server: {{ $destination->server->name }}</div>
<div class="flex flex-col mx-6">
<div class="box-title">{{ $destination->name }}</div>
<div class="box-description">server: {{ $destination->server->name }}</div>
</div>
</a>
</div>
@endif
@empty
<div>

View File

@@ -0,0 +1,9 @@
<x-emails.layout>
Scheduled task ({{ $task->name }}) was FAILED with the following error:
<pre>
{{ $output }}
</pre>
Click [here]({{ $url }}) to view the task.
</x-emails.layout>

View File

@@ -98,47 +98,6 @@
}
}
function revive() {
if (checkHealthInterval) return true;
console.log('Checking server\'s health...')
checkHealthInterval = setInterval(() => {
fetch('/api/health')
.then(response => {
if (response.ok) {
window.toast('Coolify is back online. Reloading...', {
type: 'success',
})
if (checkHealthInterval) clearInterval(checkHealthInterval);
setTimeout(() => {
window.location.reload();
}, 5000)
} else {
console.log('Waiting for server to come back from dead...');
}
})
}, 2000);
}
function upgrade() {
if (checkIfIamDeadInterval) return true;
console.log('Update initiated.')
checkIfIamDeadInterval = setInterval(() => {
fetch('/api/health')
.then(response => {
if (response.ok) {
console.log('It\'s alive. Waiting for server to be dead...');
} else {
window.toast('Update done, restarting Coolify!', {
type: 'success',
})
console.log('It\'s dead. Reviving... Standby... Bzz... Bzz...')
if (checkIfIamDeadInterval) clearInterval(checkIfIamDeadInterval);
revive();
}
})
}, 2000);
}
function copyToClipboard(text) {
navigator?.clipboard?.writeText(text) && window.Livewire.dispatch('success', 'Copied to clipboard.');
}

View File

@@ -57,8 +57,7 @@
Localhost is not reachable with the following public key.
<br /> <br />
Please make sure you have the correct public key in your ~/.ssh/authorized_keys file for
user
'root' or skip the boarding process and add a new private key manually to Coolify and to the
user or skip the boarding process and add a new private key manually to Coolify and to the
server.
<br />
Check this <a target="_blank" class="underline"
@@ -146,9 +145,12 @@
This server is not reachable with the following public key.
<br /> <br />
Please make sure you have the correct public key in your ~/.ssh/authorized_keys file for
user
'root' or skip the boarding process and add a new private key manually to Coolify and to the
user or skip the boarding process and add a new private key manually to Coolify and to the
server.
<br />
Check this <a target="_blank" class="underline"
href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further
help.
<x-forms.input readonly id="serverPublicKey"></x-forms.input>
<x-forms.button class="w-64 box-boarding" wire:target="validateServer"
wire:click="validateServer">Check
@@ -229,10 +231,6 @@
<x-forms.button type="submit">Continue</x-forms.button>
</form>
</x-slot:actions>
<x-slot:explanation>
<p>Username should be <x-highlighted text="root" /> for now. We are working on to use
non-root users.</p>
</x-slot:explanation>
</x-boarding-step>
@elseif ($currentState === 'validate-server')
<x-boarding-step title="Validate & Configure Server">

View File

@@ -15,7 +15,7 @@
checkPusherInterval = setInterval(() => {
if (window.Echo && window.Echo.connector.pusher.connection.state !== 'connected') {
checkNumber++;
if (checkNumber > 4) {
if (checkNumber > 5) {
this.popups.realtime = true;
console.error(
'Coolify could not connect to its real-time service. This will cause unusual problems on the UI if not fixed! Please check the related documentation (https://coolify.io/docs/knowledge-base/cloudflare/tunnels) or get help on Discord (https://coollabs.io/discord).)'
@@ -23,31 +23,36 @@
clearInterval(checkPusherInterval);
}
}
}, 1000);
}, 2000);
}
}
}">
@auth
<span x-show="popups.realtime === true">
<x-popup>
<x-slot:title>
<span class="font-bold text-left text-red-500">WARNING: </span>Realtime Error?!
</x-slot:title>
<x-slot:description>
<span>Coolify could not connect to its real-time service.<br>This will cause unusual problems on the UI
if
not fixed! <br><br>
Please ensure that you have opened the
<a class="underline" href='https://coolify.io/docs/knowledge-base/server/firewall' target='_blank'>required ports</a>,
check the
related <a class="underline" href='https://coolify.io/docs/knowledge-base/cloudflare/tunnels'
target='_blank'>documentation</a> or get
help on <a class="underline" href='https://coollabs.io/discord' target='_blank'>Discord</a>. </span>
</x-slot:description>
<x-slot:button-text @click="disableRealtime()">
Acknowledge & Disable This Popup
</x-slot:button-text>
</x-popup>
@if (!isCloud())
<x-popup>
<x-slot:title>
<span class="font-bold text-left text-red-500">WARNING: </span>Realtime Error?!
</x-slot:title>
<x-slot:description>
<span>Coolify could not connect to its real-time service.<br>This will cause unusual problems on the
UI
if
not fixed! <br><br>
Please ensure that you have opened the
<a class="underline" href='https://coolify.io/docs/knowledge-base/server/firewall'
target='_blank'>required ports</a>,
check the
related <a class="underline" href='https://coolify.io/docs/knowledge-base/cloudflare/tunnels'
target='_blank'>documentation</a> or get
help on <a class="underline" href='https://coollabs.io/discord' target='_blank'>Discord</a>.
</span>
</x-slot:description>
<x-slot:button-text @click="disableRealtime()">
Acknowledge & Disable This Popup
</x-slot:button-text>
</x-popup>
@endif
</span>
@endauth
<span x-show="popups.sponsorship">

View File

@@ -32,6 +32,8 @@
label="Application Deployments" />
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_database_backups"
label="Backup Status" />
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_scheduled_tasks"
label="Scheduled Tasks Status" />
</div>
@endif
</div>

View File

@@ -111,6 +111,8 @@
label="Application Deployments" />
<x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_database_backups"
label="Backup Status" />
<x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_scheduled_tasks"
label="Scheduled Tasks Status" />
</div>
@endif
</div>

View File

@@ -7,7 +7,7 @@
Save
</x-forms.button>
@if ($team->telegram_enabled)
<x-forms.button class="dark:text-white normal-case btn btn-xs no-animation btn-primary"
<x-forms.button class="normal-case dark:text-white btn btn-xs no-animation btn-primary"
wire:click="sendTestNotification">
Send Test Notifications
</x-forms.button>
@@ -18,44 +18,57 @@
</div>
<div class="flex gap-2">
<x-forms.input type="password"
helper="Get it from the <a class='inline-block dark:text-white underline' href='https://t.me/botfather' target='_blank'>BotFather Bot</a> on Telegram."
helper="Get it from the <a class='inline-block underline dark:text-white' href='https://t.me/botfather' target='_blank'>BotFather Bot</a> on Telegram."
required id="team.telegram_token" label="Token" />
<x-forms.input helper="Recommended to add your bot to a group chat and add its Chat ID here." required
id="team.telegram_chat_id" label="Chat ID" />
</div>
@if (data_get($team, 'telegram_enabled'))
<h2 class="mt-4">Subscribe to events</h2>
<div class="w-96">
<div class="flex flex-col gap-4 w-96">
@if (isDev())
<div class="w-64">
<div class="flex flex-col">
<h4>Test Notification</h4>
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_test"
label="Test" />
label="Enabled" />
<x-forms.input
helper="If you are using Group chat with Topics, you can specify the topics ID. If empty, General topic will be used."
id="team.telegram_notifications_test_message_thread_id" label="Custom Topic ID" />
</div>
@endif
<div class="w-64">
<div class="flex flex-col">
<h4>Container Status Changes</h4>
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_status_changes"
label="Container Status Changes" />
label="Enabled" />
<x-forms.input
helper="If you are using Group chat with Topics, you can specify the topics ID. If empty, General topic will be used."
id="team.telegram_notifications_status_changes_message_thread_id" label="Custom Topic ID" />
</div>
<div class="w-64">
<div class="flex flex-col">
<h4>Application Deployments</h4>
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_deployments"
label="Application Deployments" />
label="Enabled" />
<x-forms.input
helper="If you are using Group chat with Topics, you can specify the topics ID. If empty, General topic will be used."
id="team.telegram_notifications_deployments_message_thread_id" label="Custom Topic ID" />
</div>
<div class="w-64">
<div class="flex flex-col">
<h4>Database Backup Status</h4>
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_database_backups"
label="Backup Status" />
label="Enabled" />
<x-forms.input
helper="If you are using Group chat with Topics, you can specify the topics ID. If empty, General topic will be used."
id="team.telegram_notifications_database_backups_message_thread_id" label="Custom Topic ID" />
</div>
<div class="flex flex-col">
<h4>Scheduled Tasks Status</h4>
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_scheduled_tasks"
label="Enabled" />
<x-forms.input
helper="If you are using Group chat with Topics, you can specify the topics ID. If empty, General topic will be used."
id="team.telegram_notifications_scheduled_tasks_thread_id" label="Custom Topic ID" />
</div>
</div>
@endif
</form>

View File

@@ -27,18 +27,15 @@
</form>
@endif
@forelse ($deployments as $deployment)
<a @class([
'dark:bg-coolgray-100 p-2 border-l border-dashed transition-colors hover:no-underline box-without-bg-without-border bg-white flex-col',
'dark:hover:bg-coolgray-200' =>
data_get($deployment, 'status') === 'queued',
'border-warning hover:bg-warning hover:text-black' =>
<div @class([
'dark:bg-coolgray-100 p-2 border-l border-dashed transition-colors hover:no-underline box-without-bg-without-border bg-white flex-col cursor-pointer dark:hover:text-neutral-400 dark:hover:bg-coolgray-200',
'border-warning' =>
data_get($deployment, 'status') === 'in_progress' ||
data_get($deployment, 'status') === 'cancelled-by-user',
'border-error dark:hover:bg-error hover:bg-neutral-200' =>
data_get($deployment, 'status') === 'failed',
'border-success dark:hover:bg-success hover:bg-neutral-200' =>
data_get($deployment, 'status') === 'finished',
]) href="{{ $current_url . '/' . data_get($deployment, 'deployment_uuid') }}">
'border-error' => data_get($deployment, 'status') === 'failed',
'border-success' => data_get($deployment, 'status') === 'finished',
])
x-on:click.stop="goto('{{ $current_url . '/' . data_get($deployment, 'deployment_uuid') }}')">
<div class="flex flex-col justify-start">
<div class="flex gap-1">
{{ $deployment->created_at }} UTC
@@ -64,11 +61,27 @@
@endif
</div>
@else
<div class="flex gap-1">
Manual
<div class="flex items-center gap-1">
@if (data_get($deployment, 'rollback') === true)
Rollback
@else
Manual
@endif
@if (data_get($deployment, 'commit'))
<div class="dark:hover:text-white"
x-on:click.stop="goto('{{ $application->gitCommitLink(data_get($deployment, 'commit')) }}')">
<div class="text-xs underline">
@if ($deployment->commitMessage())
({{data_get_str($deployment, 'commit')->limit(7)}} - {{ $deployment->commitMessage() }})
@else
{{ data_get_str($deployment, 'commit')->limit(7) }}
@endif
</div>
</div>
@endif
</div>
@endif
@if (data_get($deployment, 'server_name'))
@if (data_get($deployment, 'server_name') && $application->additional_servers->count() > 0)
<div class="flex gap-1">
Server: {{ data_get($deployment, 'server_name') }}
</div>
@@ -85,15 +98,19 @@
<span class="font-bold" x-text="measure_finished_time()">0s</span>
</div>
</div>
</a>
</div>
@empty
<div class="">No deployments found</div>
@endforelse
@if ($deployments_count > 0)
<script src="https://cdn.jsdelivr.net/npm/dayjs@1/dayjs.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dayjs@1/plugin/utc.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dayjs@1/plugin/relativeTime.js"></script>
<script>
function goto(url) {
window.location.href = url;
};
document.addEventListener('alpine:init', () => {
let timers = {};

View File

@@ -38,7 +38,11 @@
</div>
@endif
@if ($application->build_pack === 'dockercompose')
@if (count($parsedServices) > 0 && !$application->settings->is_raw_compose_deployment_enabled)
@if (
!is_null($parsedServices) &&
count($parsedServices) > 0 &&
!$application->settings->is_raw_compose_deployment_enabled)
<h3 class="pt-6">Domains</h3>
@foreach (data_get($parsedServices, 'services') as $serviceName => $service)
@if (!isDatabaseImage(data_get($service, 'image')))
@@ -219,6 +223,11 @@
<x-forms.textarea rows="10" readonly id="application.docker_compose"
label="Docker Compose Content" helper="You need to modify the docker compose file." />
@endif
<div class="w-72">
<x-forms.checkbox label="Escape special characters in labels?"
helper="By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$.<br><br>If you want to use env variables inside the labels, turn this off."
id="application.settings.is_container_label_escape_enabled" instantSave></x-forms.checkbox>
</div>
{{-- <x-forms.textarea rows="10" readonly id="application.docker_compose_pr"
label="Docker PR Compose Content" helper="You need to modify the docker compose file." /> --}}
@endif
@@ -242,8 +251,15 @@
@endif
</div>
<x-forms.textarea label="Container Labels" rows="15" id="customLabels"></x-forms.textarea>
<x-modal-confirmation buttonFullWidth action="resetDefaultLabels" buttonTitle="Reset to Coolify Generated Labels">
Are you sure you want to reset the labels to Coolify generated labels? <br>It could break the proxy configuration after you restart the container.
<div class="w-72">
<x-forms.checkbox label="Escape special characters in labels?"
helper="By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$.<br><br>If you want to use env variables inside the labels, turn this off."
id="application.settings.is_container_label_escape_enabled" instantSave></x-forms.checkbox>
</div>
<x-modal-confirmation buttonFullWidth action="resetDefaultLabels"
buttonTitle="Reset to Coolify Generated Labels">
Are you sure you want to reset the labels to Coolify generated labels? <br>It could break the proxy
configuration after you restart the container.
</x-modal-confirmation>
@endif

View File

@@ -1,5 +1,5 @@
<nav wire:poll.5000ms="check_status">
<x-resources.breadcrumbs :resource="$application" :parameters="$parameters" />
<x-resources.breadcrumbs :resource="$application" :parameters="$parameters" :lastDeploymentInfo="$lastDeploymentInfo" :lastDeploymentLink="$lastDeploymentLink" />
<div class="navbar-main">
<nav class="flex items-center flex-shrink-0 gap-6 scrollbar min-h-10 whitespace-nowrap">
<a href="{{ route('project.application.configuration', $parameters) }}">

View File

@@ -19,22 +19,25 @@
<h2 class="pt-4 pb-4">Select a Github App</h2>
<div class="flex flex-col gap-2">
@if ($current_step === 'github_apps')
<div class="flex flex-row justify-center gap-2 text-left">
<div class="flex flex-col justify-center gap-2 text-left">
@foreach ($github_apps as $ghapp)
<div class="w-full gap-2 py-4 bg-white cursor-pointer group hover:bg-coollabs dark:bg-coolgray-200 box"
wire:click.prevent="loadRepositories({{ $ghapp->id }})" wire:key="{{ $ghapp->id }}">
<div class="flex mr-4">
<div class="flex flex-col mx-6">
<div class="box-title">
{{ data_get($ghapp, 'name') }}
<div class="flex">
<div class="w-full gap-2 py-4 bg-white cursor-pointer group hover:bg-coollabs dark:bg-coolgray-200 box"
wire:click.prevent="loadRepositories({{ $ghapp->id }})"
wire:key="{{ $ghapp->id }}">
<div class="flex mr-4">
<div class="flex flex-col mx-6">
<div class="box-title">
{{ data_get($ghapp, 'name') }}
</div>
<div class="box-description">
{{ data_get($ghapp, 'html_url') }}</div>
</div>
<div class="box-description">
{{ data_get($ghapp, 'html_url') }}</div>
</div>
</div>
</div>
<div class="flex flex-col items-center justify-center">
<x-loading wire:loading wire:target="loadRepositories({{ $ghapp->id }})" />
<div class="flex flex-col items-center justify-center">
<x-loading wire:loading wire:target="loadRepositories({{ $ghapp->id }})" />
</div>
</div>
@endforeach
</div>

View File

@@ -12,7 +12,7 @@
</div>
@if (!$branch_found)
<div class="px-2 pt-4">
<div class="flex gap-1">
<div class="flex flex-col pb-4">
<div>Preselect branch (eg: main):</div>
<div class='text-helper'>https://github.com/coollabsio/coolify-examples/tree/main</div>
</div>

View File

@@ -16,6 +16,10 @@
@click.prevent="activeTab = 'storages';
window.location.hash = 'storages'"
href="#">Storages</a>
<a class="menu-item" :class="activeTab === 'scheduled-tasks' && 'menu-item-active'"
@click.prevent="activeTab = 'scheduled-tasks'; window.location.hash = 'scheduled-tasks'"
href="#">Scheduled Tasks
</a>
<a class="menu-item sm:min-w-fit" :class="activeTab === 'execute-command' && 'menu-item-active'"
@click.prevent="activeTab = 'execute-command';
window.location.hash = 'execute-command'"
@@ -103,10 +107,13 @@
href="{{ route('project.service.index', [...$parameters, 'stack_service_uuid' => $application->uuid]) }}">
Settings
</a>
<x-modal-confirmation action="restartApplication({{ $application->id }})"
isErrorButton buttonTitle="Restart">
This application will be unavailable during the restart. <br>Please think again.
</x-modal-confirmation>
@if (str($application->status)->contains('running'))
<x-modal-confirmation action="restartApplication({{ $application->id }})"
isErrorButton buttonTitle="Restart">
This application will be unavailable during the restart. <br>Please think
again.
</x-modal-confirmation>
@endif
</div>
</div>
</div>
@@ -169,6 +176,9 @@
<livewire:project.service.storage wire:key="database-{{ $database->id }}" :resource="$database" />
@endforeach
</div>
<div x-cloak x-show="activeTab === 'scheduled-tasks'">
<livewire:project.shared.scheduled-task.all :resource="$service" />
</div>
<div x-cloak x-show="activeTab === 'webhooks'">
<livewire:project.shared.webhooks :resource="$service" />
</div>

View File

@@ -3,6 +3,11 @@
prevent
name collision. <br>To see the actual volume names, check the Deployable Compose file, or go to Storage
menu.</div>
<div class="pb-2 w-72">
<x-forms.checkbox label="Escape special characters in labels?"
helper="By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$.<br><br>If you want to use env variables inside the labels, turn this off."
id="service.is_container_label_escape_enabled" instantSave></x-forms.checkbox>
</div>
<div x-cloak x-show="raw" class="font-mono">
<x-forms.textarea allowTab rows="20" id="service.docker_compose_raw">
</x-forms.textarea>
@@ -14,10 +19,10 @@
<div class="flex justify-end w-full gap-2 pt-4">
<div class="flex items-end gap-2">
<div x-cloak x-show="raw">
<x-forms.button class="w-64" @click.prevent="raw = !raw">Show Deployable Compose</x-forms.button>
<x-forms.button class="w-64" @click.prevent="raw = !raw">Show Deployable Compose</x-forms.button>
</div>
<div x-cloak x-show="raw === false">
<x-forms.button class="w-64" @click.prevent="raw = !raw">Show Source
<x-forms.button class="w-64" @click.prevent="raw = !raw">Show Source
Compose</x-forms.button>
</div>
</div>

View File

@@ -10,14 +10,6 @@
<a class="menu-item" :class="activeTab === 'general' && 'menu-item-active'"
@click.prevent="activeTab = 'general'; window.location.hash = 'general'; if(window.location.search) window.location.search = ''"
href="#">General</a>
<a class="menu-item" :class="activeTab === 'storages' && 'menu-item-active'"
@click.prevent="activeTab = 'storages'; window.location.hash = 'storages'; if(window.location.search) window.location.search = ''"
href="#">Storages
</a>
<a class="menu-item" :class="activeTab === 'scheduled-tasks' && 'menu-item-active'"
@click.prevent="activeTab = 'scheduled-tasks'; window.location.hash = 'scheduled-tasks'"
href="#">Scheduled Tasks
</a>
@if (str($serviceDatabase?->databaseType())->contains('mysql') ||
str($serviceDatabase?->databaseType())->contains('postgres') ||
str($serviceDatabase?->databaseType())->contains('mariadb'))
@@ -30,28 +22,12 @@
<div x-cloak x-show="activeTab === 'general'" class="h-full">
<livewire:project.service.service-application-view :application="$serviceApplication" />
</div>
<div x-cloak x-show="activeTab === 'storages'">
<div class="flex items-center gap-2">
<h2>Storages</h2>
</div>
<div class="pb-4">Persistent storage to preserve data between deployments.</div>
<span class="dark:text-warning">Please modify storage layout in your Docker Compose file.</span>
<livewire:project.service.storage wire:key="application-{{ $serviceApplication->id }}"
:resource="$serviceApplication" />
</div>
@endisset
@isset($serviceDatabase)
<div x-cloak x-show="activeTab === 'general'" class="h-full">
<livewire:project.service.database :database="$serviceDatabase" />
</div>
<div x-cloak x-show="activeTab === 'storages'">
<div class="flex items-center gap-2">
<h2>Storages</h2>
</div>
<div class="pb-4">Persistent storage to preserve data between deployments.</div>
<span class="dark:text-warning">Please modify storage layout in your Docker Compose file.</span>
<livewire:project.service.storage wire:key="application-{{ $serviceDatabase->id }}" :resource="$serviceDatabase" />
</div>
<div x-cloak x-show="activeTab === 'backups'">
<div class="flex gap-2 ">
<h2 class="pb-4">Scheduled Backups</h2>
@@ -62,9 +38,6 @@
<livewire:project.database.scheduled-backups :database="$serviceDatabase" />
</div>
@endisset
<div x-cloak x-show="activeTab === 'scheduled-tasks'">
<livewire:project.shared.scheduled-task.all :resource="$service" />
</div>
</div>
</div>
</div>

View File

@@ -11,12 +11,18 @@
wire:click='switch'>{{ $view === 'normal' ? 'Developer view' : 'Normal view' }}</x-forms.button>
</div>
<div>Environment variables (secrets) for this resource.</div>
@if ($this->resourceClass === 'App\Models\Application' && data_get($this->resource, 'build_pack') !== 'dockercompose')
<div class="w-64 pt-2">
<x-forms.checkbox id="resource.settings.is_env_sorting_enabled" label="Sort alphabetically"
helper="Turn this off if one environment is dependent on an other. It will be sorted by creation order." instantSave></x-forms.checkbox>
</div>
@endif
@if ($resource->type() === 'service' || $resource?->build_pack === 'dockercompose')
<div class="pt-4 dark:text-warning text-coollabs">Hardcoded variables are not shown here.</div>
@endif
</div>
@if ($view === 'normal')
@forelse ($resource->environment_variables->sort()->sortBy('key') as $env)
@forelse ($resource->environment_variables as $env)
<livewire:project.shared.environment-variable.show wire:key="environment-{{ $env->id }}"
:env="$env" :type="$resource->type()" />
@empty
@@ -27,7 +33,7 @@
<h3>Preview Deployments</h3>
<div>Environment (secrets) variables for Preview Deployments.</div>
</div>
@foreach ($resource->environment_variables_preview->sort()->sortBy('key') as $env)
@foreach ($resource->environment_variables_preview as $env)
<livewire:project.shared.environment-variable.show wire:key="environment-{{ $env->id }}"
:env="$env" :type="$resource->type()" />
@endforeach

View File

@@ -1,10 +1,27 @@
<form class="flex flex-col w-full gap-2 rounded" wire:submit='submit'>
<x-forms.input autofocus placeholder="Run cron" id="name" label="Name" />
<x-forms.input placeholder="php artisan schedule:run" id="command" label="Command" />
<x-forms.input placeholder="0 0 * * * or daily" id="frequency" label="Frequency" />
<x-forms.input placeholder="php" id="container"
helper="You can leave it empty if your resource only have one container." label="Container name" />
<x-forms.button @click="slideOverOpen=false" type="submit">
<x-forms.input autofocus placeholder="Run cron" id="name" label="Name" />
<x-forms.input placeholder="php artisan schedule:run" id="command" label="Command" />
<x-forms.input placeholder="0 0 * * * or daily" id="frequency" label="Frequency" />
@if ($type === 'application')
@if ($containerNames->count() > 1)
<x-forms.select id="container" label="Container name">
@foreach ($containerNames as $containerName)
<option value="{{ $containerName }}">{{ $containerName }}</option>
@endforeach
</x-forms.select>
@else
<x-forms.input placeholder="php" id="container"
helper="You can leave it empty if your resource only have one container." label="Container name" />
@endif
@elseif ($type === 'service')
<x-forms.select id="container" label="Container name">
@foreach ($containerNames as $containerName)
<option value="{{ $containerName }}">{{ $containerName }}</option>
@endforeach
</x-forms.select>
@endif
<x-forms.button @click="modalOpen=false" type="submit">
Save
</x-forms.button>
</form>

View File

@@ -1,23 +1,48 @@
<div>
<div class="flex gap-2">
<h2>Scheduled Tasks</h2>
<x-modal-input buttonTitle="+ Add" title="New Scheduled Task">
<livewire:project.shared.scheduled-task.add />
<x-modal-input buttonTitle="+ Add" title="New Scheduled Task" :closeOutside=false>
@if ($resource->type() == 'application')
<livewire:project.shared.scheduled-task.add :type="$resource->type()" :containerNames="$containerNames"/>
@elseif ($resource->type() == 'service')
<livewire:project.shared.scheduled-task.add :type="$resource->type()" :containerNames="$containerNames"/>
@endif
</x-modal-input>
</div>
<div class="flex flex-wrap gap-2 pt-4">
@forelse($resource->scheduled_tasks as $task)
<a class="flex flex-col box"
@if ($resource->type() == 'application') href="{{ route('project.application.scheduled-tasks', [...$parameters, 'task_uuid' => $task->uuid]) }}">
@elseif ($resource->type() == 'service')
href="{{ route('project.service.scheduled-tasks', [...$parameters, 'task_uuid' => $task->uuid]) }}"> @endif
<div><span class="font-bold dark:text-warning">{{ $task->name }}<span>
@if ($resource->type() == 'application')
<a class="box"
href="{{ route('project.application.scheduled-tasks', [...$parameters, 'task_uuid' => $task->uuid]) }}">
<span class="flex flex-col">
<span class="text-lg font-bold">{{ $task->name }}
@if ($task->container)
<span class="text-xs font-normal">({{ $task->container }})</span>
@endif
</span>
<span>Frequency: {{ $task->frequency }}</span>
<span>Last run: {{ data_get($task->latest_log, 'status', 'No runs yet') }}
</span>
</span>
</a>
@elseif ($resource->type() == 'service')
<a class="box"
href="{{ route('project.service.scheduled-tasks', [...$parameters, 'task_uuid' => $task->uuid]) }}">
<span class="flex flex-col">
<span class="text-lg font-bold">{{ $task->name }}
@if ($task->container)
<span class="text-xs font-normal">({{ $task->container }})</span>
@endif
</span>
<span>Frequency: {{ $task->frequency }}</span>
<span>Last run: {{ data_get($task->latest_log, 'status', 'No runs yet') }}
</span>
</span>
</a>
@endif
@empty
<div>No scheduled tasks configured.</div>
@endforelse
</div>
<div>Frequency: {{ $task->frequency }}</div>
<div>Last run: {{ data_get($task->latest_log, 'status', 'No runs yet') }}</div>
</a>
@empty
<div>No scheduled tasks configured.</div>
@endforelse
</div>
</div>

View File

@@ -12,7 +12,7 @@
</div>
@endif
<a wire:click="selectTask({{ data_get($execution, 'id') }})" @class([
'flex flex-col border-l border-dashed transition-colors box-without-bg bg-coolgray-100 hover:bg-coolgray-100',
'flex flex-col border-l transition-colors box-without-bg bg-coolgray-100 hover:bg-coolgray-200 cursor-pointer',
'bg-coolgray-200 dark:text-white hover:bg-coolgray-200' =>
data_get($execution, 'id') == $selectedKey,
'border-green-500' => data_get($execution, 'status') === 'success',

View File

@@ -1,13 +1,13 @@
<div>
<h1>Scheduled Task</h1>
@if ($type === 'application')
<h1>Scheduled Task</h1>
<livewire:project.application.heading :application="$resource" />
@elseif ($type === 'service')
<livewire:project.service.navbar :service="$resource" :parameters="$parameters" />
@endif
<form wire:submit="submit" class="w-full">
<div class="flex flex-col gap-2 pb-4">
<div class="flex flex-col gap-2 pb-2">
<div class="flex items-end gap-2 pt-4">
<h2>Scheduled Task</h2>
<x-forms.button type="submit">
@@ -17,13 +17,23 @@
You will delete scheduled task <span class="font-bold dark:text-warning">{{ $task->name }}</span>.
</x-modal-confirmation>
</div>
<div class="w-48">
<x-forms.checkbox instantSave id="task.enabled" label="Enabled" />
</div>
</div>
<div class="flex w-full gap-2">
<x-forms.input placeholder="Name" id="task.name" label="Name" required />
<x-forms.input placeholder="php artisan schedule:run" id="task.command" label="Command" required />
<x-forms.input placeholder="0 0 * * * or daily" id="task.frequency" label="Frequency" required />
<x-forms.input placeholder="php" helper="You can leave it empty if your resource only have one container."
id="task.container" label="Container name" />
@if ($type === 'application')
<x-forms.input placeholder="php"
helper="You can leave it empty if your resource only have one container." id="task.container"
label="Container name" />
@elseif ($type === 'service')
<x-forms.input placeholder="php"
helper="You can leave it empty if your resource only have one service in your stack. Otherwise use the stack name, without the random generated id. So if you have a mysql service in your stack, use mysql."
id="task.container" label="Service name" />
@endif
</div>
</form>

View File

@@ -1,7 +1,7 @@
<div class="flex gap-2">
<h3 class="dark:text-white">File: {{ str_replace('|', '.', $fileName) }}</h3>
<div class="flex gap-2">
<x-modal-input buttonTitle="+ Add" title="Edit Configuration">
<x-modal-input buttonTitle="Edit" title="Edit Configuration">
<livewire:server.proxy.new-dynamic-configuration :server_id="$server_id" :fileName="$fileName" :value="$value"
:newFile="$newFile" wire:key="{{ $fileName }}" />
</x-modal-input>

View File

@@ -9,7 +9,7 @@
<div>General configuration for your Coolify instance.</div>
<div class="flex flex-col gap-2 pt-4">
<div class="flex items-end gap-2 flex-wrap">
<div class="flex flex-wrap items-end gap-2">
<x-forms.input id="settings.fqdn" label="Instance's Domain" placeholder="https://coolify.io" />
<x-forms.input id="settings.custom_dns_servers" label="DNS Servers"
helper="DNS servers for validation FQDNs againts. A comma separated list of DNS servers."
@@ -35,13 +35,13 @@
@endif
<x-forms.checkbox instantSave id="is_registration_enabled" label="Registration Allowed" />
<x-forms.checkbox instantSave id="do_not_track" label="Do Not Track" />
@if ($next_channel)
{{-- @if ($next_channel)
<x-forms.checkbox instantSave helper="Not recommended. Only if you like to live on the edge."
id="next_channel" label="Enable pre-release (early) updates" />
@else
<x-forms.checkbox disabled instantSave
helper="Currently disabled. Not recommended. Only if you like to live on the edge." id="next_channel"
label="Enable pre-release (early) updates" />
@endif
@endif --}}
</div>
</div>

View File

@@ -1,12 +1,11 @@
<div>
<div class="subtitle">S3 Storage used to save backups / files.</div>
<div class="w-full">
<form class="flex flex-col gap-2" wire:submit='submit'>
<div class="flex gap-2">
<x-forms.input required label="Name" id="name" />
<x-forms.input label="Description" id="description" />
</div>
<x-forms.input required type="url" label="Endpoint" id="endpoint" />
<div class="flex gap-2">
<x-forms.input required type="url" label="Endpoint" id="endpoint" />
<x-forms.input required label="Bucket" id="bucket" />
<x-forms.input required label="Region" id="region" />
</div>

View File

@@ -1,7 +1,7 @@
<div>
<div class="flex items-start gap-2">
<h1>S3 Storages</h1>
<x-modal-input buttonTitle="+ Add" title="New S3 Storage">
<x-modal-input buttonTitle="+ Add" title="New S3 Storage" :closeOutside="false">
<livewire:storage.create />
</x-modal-input>
</div>

View File

@@ -1,5 +1,5 @@
<div x-data="{ selected: 'monthly' }" class="w-full pb-20">
<div class="px-6 mx-auto lg:px-8">
<div class="px-6 mx-auto lg:px-8">
<div class="flex justify-center">
<fieldset
class="grid grid-cols-2 p-1 text-xs font-semibold leading-5 text-center rounded dark:text-white gap-x-1 dark:bg-white/5 bg-black/5">
@@ -62,7 +62,7 @@
></path></svg
>
<div class="flex flex-col text-sm text-white">
<div class="flex flex-col text-sm dark:text-white">
<div>
You need to bring your own servers from any cloud provider (such as <a
class="underline"

View File

@@ -0,0 +1,29 @@
<div>
<x-team.navbar />
<form wire:submit="submitSearch" class="flex flex-col gap-2 lg:flex-row">
<x-forms.input wire:model="search" placeholder="Search for a user" />
<x-forms.button type="submit">Search</x-forms.button>
</form>
<h3 class="pt-4">Users</h3>
<div class="flex flex-col gap-2 ">
@forelse ($users as $user)
<div class="flex items-center justify-center gap-2 bg-white box-without-bg dark:bg-coolgray-100">
<div>{{ $user->name }}</div>
<div>{{ $user->email }}</div>
<div class="flex-1"></div>
<div class="flex items-center justify-center gap-2 mx-4 text-xs font-bold ">
<x-modal-confirmation isErrorButton action="delete({{ $user->id }})" buttonTitle="Delete">
This will delete all resources (application, databases, services, configurations, servers,
private keys, tags, etc.) from Coolify and <span
class="font-bold text-red-500 dark:text-warning">from the server (if it's reachable)</span>.
<br> <br>
It is not reversible. <br><br>
<div class="font-bold text-red-500 dark:text-white">Think twice!</div>
</x-modal-confirmation>
</div>
</div>
@empty
<div>No users found other than the root.</div>
@endforelse
</div>
</div>

View File

@@ -1,5 +1,5 @@
<div>
<form wire:submit='viaLink' class="flex items-center gap-2">
<form wire:submit='viaLink' class="flex flex-col gap-2 lg:items-center lg:flex-row">
<x-forms.input id="email" type="email" name="email" placeholder="Email" />
<x-forms.select id="role" name="role">
<option value="owner">Owner</option>

View File

@@ -1,29 +1,139 @@
<div @if ($isUpgradeAvailable) title="New version available" @else title="No upgrade available" @endif
x-init="$wire.checkUpdate" x-data>
x-init="$wire.checkUpdate" x-data="upgradeModal">
@if ($isUpgradeAvailable)
<button wire:key="upgrade" wire:click='upgrade' class="menu-item" x-on:click="upgrade">
@if ($showProgress)
<svg xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 text-pink-500 transition-colors hover:text-pink-300 lds-heart" 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="M19.5 13.572l-7.5 7.428l-7.5 -7.428m0 0a5 5 0 1 1 7.5 -6.566a5 5 0 1 1 7.5 6.572" />
</svg>
In progress
@else
<svg xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 text-pink-500 transition-colors hover:text-pink-300" 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="M9 12h-3.586a1 1 0 0 1 -.707 -1.707l6.586 -6.586a1 1 0 0 1 1.414 0l6.586 6.586a1 1 0 0 1 -.707 1.707h-3.586v3h-6v-3z" />
<path d="M9 21h6" />
<path d="M9 18h6" />
</svg>
Upgrade
@endif
</button>
<div :class="{ 'z-40': modalOpen }" class="relative w-auto h-auto">
<button class="menu-item" @click="modalOpen=true">
@if ($showProgress)
<svg xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 text-pink-500 transition-colors hover:text-pink-300 lds-heart" 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="M19.5 13.572l-7.5 7.428l-7.5 -7.428m0 0a5 5 0 1 1 7.5 -6.566a5 5 0 1 1 7.5 6.572" />
</svg>
In progress
@else
<svg xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 text-pink-500 transition-colors hover:text-pink-300" 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="M9 12h-3.586a1 1 0 0 1 -.707 -1.707l6.586 -6.586a1 1 0 0 1 1.414 0l6.586 6.586a1 1 0 0 1 -.707 1.707h-3.586v3h-6v-3z" />
<path d="M9 21h6" />
<path d="M9 18h6" />
</svg>
Upgrade
@endif
</button>
<template x-teleport="body">
<div x-show="modalOpen"
class="fixed top-0 lg:pt-10 left-0 z-[99] flex items-start justify-center w-screen h-screen"
x-cloak>
<div x-show="modalOpen" x-transition:enter="ease-out duration-100"
x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
x-transition:leave="ease-in duration-100" x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
class="absolute inset-0 w-full h-full bg-black bg-opacity-20 backdrop-blur-sm"></div>
<div x-show="modalOpen" x-trap.inert.noscroll="modalOpen" x-transition:enter="ease-out duration-100"
x-transition:enter-start="opacity-0 -translate-y-2 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave="ease-in duration-100"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
class="relative w-full py-6 border rounded min-w-full lg:min-w-[36rem] max-w-fit bg-neutral-100 border-neutral-400 dark:bg-base px-7 dark:border-coolgray-300">
<div class="flex items-center justify-between pb-3">
<h3 class="text-lg font-semibold">Upgrade confirmation</h3>
@if (!$showProgress)
<button @click="modalOpen=false"
class="absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5 text-gray-600 rounded-full hover:text-gray-800 hover:bg-gray-50">
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
@endif
</div>
<div class="relative w-auto pb-8">
<p>Are you sure you would like to upgrade your instance to {{ $latestVersion }}?</p>
<br />
<p>You can review the changelogs <a class="font-bold underline"
href="https://github.com/coollabsio/coolify/releases" target="_blank">here</a>.</p>
@if ($showProgress)
<div class="flex flex-col pt-4">
<h4>Progress <x-loading /></h4>
<div x-html="currentStatus"></div>
</div>
@endif
</div>
<div class="flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2">
@if (!$showProgress)
<x-forms.button @click="modalOpen=false"
class="w-24 dark:bg-coolgray-200 dark:hover:bg-coolgray-300">Cancel
</x-forms.button>
<x-forms.button @click="confirmed" class="w-24" isHighlighted type="button">Continue
</x-forms.button>
@endif
</div>
</div>
</div>
</template>
</div>
@endif
</div>
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('upgradeModal', () => ({
modalOpen: false,
showProgress: @js($showProgress),
currentStatus: '',
confirmed() {
this.$wire.$call('upgrade')
this.upgrade();
this.$wire.showProgress = true;
},
revive() {
if (checkHealthInterval) return true;
console.log('Checking server\'s health...')
checkHealthInterval = setInterval(() => {
fetch('/api/health')
.then(response => {
if (response.ok) {
this.currentStatus =
'Coolify is back online. Reloading this page (you can manually reload if its not done automatically)...';
if (checkHealthInterval) clearInterval(
checkHealthInterval);
setTimeout(() => {
window.location.reload();
}, 5000)
} else {
this.currentStatus =
"Waiting for Coolify to come back from dead..."
}
})
}, 2000);
},
upgrade() {
if (checkIfIamDeadInterval || this.$wire.showProgress) return true;
this.currentStatus = 'Pulling new images and updating Coolify.';
checkIfIamDeadInterval = setInterval(() => {
fetch('/api/health')
.then(response => {
if (response.ok) {
this.currentStatus = "Waiting for the update process..."
} else {
this.currentStatus =
"Update done, restarting Coolify & waiting until it is revived!"
if (checkIfIamDeadInterval) clearInterval(
checkIfIamDeadInterval);
this.revive();
}
})
}, 2000);
}
}))
})
</script>

View File

@@ -82,7 +82,7 @@ use App\Livewire\Subscription\Show as SubscriptionShow;
use App\Livewire\Tags\Index as TagsIndex;
use App\Livewire\Tags\Show as TagsShow;
use App\Livewire\Team\AdminView as TeamAdminView;
use App\Livewire\Waitlist\Index as WaitlistIndex;
use App\Models\ScheduledDatabaseBackupExecution;
use Illuminate\Support\Facades\Storage;
@@ -160,6 +160,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::prefix('team')->group(function () {
Route::get('/', TeamIndex::class)->name('team.index');
Route::get('/members', TeamMemberIndex::class)->name('team.member.index');
Route::get('/admin', TeamAdminView::class)->name('team.admin-view');
});
Route::get('/command-center', CommandCenterIndex::class)->name('command-center');

View File

@@ -7,7 +7,7 @@ set -e # Exit immediately if a command exits with a non-zero status
set -o pipefail # Cause a pipeline to return the status of the last command that exited with a non-zero status
VERSION="1.3.1"
DOCKER_VERSION="24.0"
DOCKER_VERSION="26.0"
CDN="https://cdn.coollabs.io/coolify"
OS_TYPE=$(grep -w "ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
@@ -135,7 +135,6 @@ if [ "$SSH_PERMIT_ROOT_LOGIN" != "true" ]; then
echo "WARNING: PermitRootLogin is not enabled in /etc/ssh/sshd_config."
echo -e "It is set to $SSH_PERMIT_ROOT_LOGIN_CONFIG. Should be prohibit-password, yes or without-password.\n"
echo -e "Please make sure it is set, otherwise Coolify cannot connect to the host system. \n"
echo "(Currently we only support root user to login via SSH, this will be changed in the future.)"
echo "###############################################################################"
fi

View File

@@ -32,7 +32,7 @@ services:
redis:
condition: service_started
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:80"]
test: ["CMD", "curl", "-f", "http://127.0.0.1:80"]
interval: 5s
timeout: 20s
retries: 10

View File

@@ -16,7 +16,7 @@ services:
volumes:
- stacks-data:/appsmith-stacks
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:80"]
test: ["CMD", "curl", "-f", "http://127.0.0.1:80"]
interval: 5s
timeout: 20s
retries: 10

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