Compare commits

...

123 Commits

Author SHA1 Message Date
Andras Bacsai
b983b23e7e Merge pull request #1766 from coollabsio/next
v4.0.0-beta.221
2024-02-19 13:29:58 +01:00
Andras Bacsai
0b81e77a94 fix: database status 2024-02-19 13:28:14 +01:00
Andras Bacsai
b8cf314bfe fix: submodule cloning 2024-02-19 13:22:09 +01:00
Andras Bacsai
4a3338e59c Update version numbers 2024-02-19 10:44:52 +01:00
Andras Bacsai
5b8538c0f4 Merge pull request #1763 from coollabsio/next
v4.0.0-beta.220
2024-02-19 09:53:54 +01:00
Andras Bacsai
88d6320d08 Re-enable docker container removal in ApplicationDeploymentJob 2024-02-19 09:51:39 +01:00
Andras Bacsai
651c9c2c9b Merge pull request #1756 from victor-teles/fix/log-drain-parsers
fix: fluent bit parsers.conf indentation level
2024-02-19 09:22:36 +01:00
Andras Bacsai
8dd45cd388 Merge pull request #1757 from victor-teles/fix/revalidate-server-button
fix(server): revalidate server button not showing in server's page
2024-02-19 09:21:50 +01:00
Andras Bacsai
126ac354d5 fix: empty build variables 2024-02-19 09:19:50 +01:00
Victor
024769c402 fix(server): revalidate server button not showing in server's page 2024-02-17 12:43:49 -03:00
Victor
5acf141669 fix: fluent bit ident level 2024-02-17 12:25:48 -03:00
Andras Bacsai
92e3e8ab7b Merge branch 'main' into next 2024-02-17 16:17:01 +01:00
Andras Bacsai
187a29c666 Update README.md 2024-02-17 16:16:42 +01:00
Andras Bacsai
4c24631795 Add coolify.managed flag to proxy configuration 2024-02-16 23:17:29 +01:00
Andras Bacsai
7d6bd10cca Add Docker container management methods and update Livewire component 2024-02-16 23:09:35 +01:00
Andras Bacsai
f8c86769a7 fix: resources 2024-02-16 22:15:18 +01:00
Andras Bacsai
e0b0dda382 Remove unused code for displaying server resources 2024-02-16 22:04:26 +01:00
Andras Bacsai
b8708f086e feat: initial api endpoints
feat: server resources are now looks better
2024-02-16 21:56:38 +01:00
Andras Bacsai
3539e4dce9 Update Docker installation error messages 2024-02-16 09:06:28 +01:00
Andras Bacsai
5fdadcf557 fix: add openbsd ssh server check 2024-02-16 09:04:32 +01:00
Andras Bacsai
acb3f01f79 Merge pull request #1752 from ahmedrowaihi/main
👌 IMPORVE(SCRIPT/INSTALL): Support Archlinux
2024-02-16 09:03:51 +01:00
Andras Bacsai
83becdb19d fix: only show redeployment required if status is not exited 2024-02-16 08:34:30 +01:00
=
e3e8fe7895 👌 IMPORVE(SCRIPT/INSTALL): Support Archlinux 2024-02-16 01:56:16 +03:00
Andras Bacsai
6ddff8fae1 Merge pull request #1748 from coollabsio/next
v4.0.0-beta.219
2024-02-15 21:33:50 +01:00
Andras Bacsai
f5cb2dbdcf Fix condition in removeServer method 2024-02-15 21:25:43 +01:00
Andras Bacsai
fe19769d82 fix: do not add the same server twice 2024-02-15 21:22:59 +01:00
Andras Bacsai
45e404b15b feat: disable gzip compression on service applications 2024-02-15 20:44:01 +01:00
Andras Bacsai
5bdaa68368 Add docker-registry service template and update service-templates.json 2024-02-15 15:39:27 +01:00
Andras Bacsai
d903a377bf Update validation and configuration titles 2024-02-15 14:14:11 +01:00
Andras Bacsai
8e7745f4c1 Remove unnecessary debug statement in Server.php 2024-02-15 13:54:18 +01:00
Andras Bacsai
a9ea6330d9 feat: revalidate server 2024-02-15 13:52:54 +01:00
Andras Bacsai
bfb0260550 fix: use ls / command instead ls 2024-02-15 13:52:42 +01:00
Andras Bacsai
bba1cb3832 fix: ec2 does not have uptime command lol
version++
2024-02-15 13:44:40 +01:00
Andras Bacsai
29ad2144b7 Merge pull request #1747 from coollabsio/next
v4.0.0-beta.218
2024-02-15 12:59:51 +01:00
Andras Bacsai
38d367e709 fix: padding left on input boxes 2024-02-15 12:59:25 +01:00
Andras Bacsai
0e81ff970f Merge pull request #1746 from coollabsio/next
v4.0.0-beta.217
2024-02-15 12:54:38 +01:00
Andras Bacsai
00feef40a3 Fix image tag in docker-compose.prod.yml 2024-02-15 12:29:30 +01:00
Andras Bacsai
dfba593072 feat: magic for traefik redirectregex in services 2024-02-15 12:08:48 +01:00
Andras Bacsai
c770c8d988 Add warning icon for configuration not applied 2024-02-15 12:04:52 +01:00
Andras Bacsai
0f071031a9 Refactor application FQDN handling 2024-02-15 12:01:59 +01:00
Andras Bacsai
99efa857f4 feat: add metabase
feat: consistent container names
fix: for services, you only need to add basicauth label, others are added by coolify
fix: label uuids are not randomly generated all the time
fix: changing force https will change the labels
2024-02-15 11:55:43 +01:00
Andras Bacsai
80035395ff Update version numbers + do not cleanup queue on cloud 2024-02-14 15:31:43 +01:00
Andras Bacsai
d3490e1c95 Merge pull request #1743 from coollabsio/next
v4.0.0-beta.216
2024-02-14 15:21:13 +01:00
Andras Bacsai
dab13c92eb Update disk_usage property type in ServerStatusJob 2024-02-14 15:21:03 +01:00
Andras Bacsai
1f18542960 fix: cleanup scheduled tasks 2024-02-14 15:14:06 +01:00
Andras Bacsai
8f21ea9367 Merge pull request #1727 from lxix/fix-scheduled-tasks
fix: Scheduled Tasks won't execute after deleting resource with scheduled task
2024-02-14 15:02:09 +01:00
Andras Bacsai
73e64d9052 fix: file volume creation
fix: network_mode host compose
2024-02-14 15:00:24 +01:00
Andras Bacsai
6cdd87da41 Update Directus image version to 10 2024-02-14 14:59:38 +01:00
Andras Bacsai
2a5d49f9b3 Merge pull request #1731 from notskamr/patch-1
Update directus-with-postgresql.yaml - Version bump
2024-02-14 14:58:43 +01:00
Andras Bacsai
cc7ba9eb9f Merge pull request #1734 from Geczy/fix-mg
fix: only add 'networks' key if 'network_mode' is absent
2024-02-14 14:31:48 +01:00
Andras Bacsai
c4cc42c8d5 Update version and release numbers 2024-02-14 10:35:44 +01:00
Andras Bacsai
2d0838b112 Merge pull request #1742 from coollabsio/next
v4.0.0-beta.215
2024-02-14 10:32:48 +01:00
Andras Bacsai
07b94a8e48 Update filebrowser.yaml and service-templates.json 2024-02-14 10:31:05 +01:00
Andras Bacsai
4b08abc144 Save storage on initial creation 2024-02-14 10:21:53 +01:00
Andras Bacsai
93e4fc2f32 Update filebrowser image and volume bindings 2024-02-14 10:14:14 +01:00
Andras Bacsai
6dd86eec30 Fix directory creation issue in LocalFileVolume.php and parseDockerComposeFile() 2024-02-14 10:13:49 +01:00
Andras Bacsai
a7ab5d55d3 Add syncthing data volumes and update syncthing service template 2024-02-14 10:00:27 +01:00
Andras Bacsai
689547463c Merge pull request #1712 from RayBB/syncthing-template
add Syncthing template
2024-02-14 09:50:50 +01:00
Andras Bacsai
8a0046c571 update packages + fix tests 2024-02-14 09:21:25 +01:00
Andras Bacsai
fb3991321a Update Coolify version and Sentry configuration 2024-02-14 08:53:13 +01:00
Andras Bacsai
ca6543a919 Merge pull request #1741 from coollabsio/next
v4.0.0-beta.214
2024-02-14 08:44:42 +01:00
Andras Bacsai
364a6aa3a2 fix: boolean docker options 2024-02-14 08:42:47 +01:00
Andras Bacsai
82b0667277 Update version numbers 2024-02-12 12:56:04 +01:00
Andras Bacsai
74c126c731 Merge pull request #1729 from coollabsio/next
v4.0.0-beta.213
2024-02-12 11:56:40 +01:00
Andras Bacsai
d87a0fe74f Refactor authentication check in Index.php 2024-02-12 11:53:28 +01:00
Andras Bacsai
9a858f628d Merge pull request #1732 from fipnooone/fix/previews-flex-wrap
Flex-wrap deployment previews
2024-02-12 11:50:41 +01:00
Andras Bacsai
fed01fa9d2 Fix subscription retrieval and handle missing subscriptions 2024-02-12 11:48:28 +01:00
Andras Bacsai
e1468da36a feat: add proxy start to server validation
fix: boarding flow updated
2024-02-12 11:46:36 +01:00
Andras Bacsai
ddfc1440cd fix: menu 2024-02-12 10:05:45 +01:00
Andras Bacsai
5fc46384e6 Refactor status component to exclude parentheses in status message 2024-02-11 18:08:36 +01:00
Andras Bacsai
48d9df1e43 Add conditional display of deployment server name in previews.blade.php 2024-02-11 17:29:14 +01:00
Andras Bacsai
6b62d91f82 Update service_name parameter to stack_service_uuid in index.blade.php 2024-02-11 17:24:20 +01:00
Matt
e6ca8cd167 fix: only add 'networks' key if 'network_mode' is absent 2024-02-11 09:22:09 -06:00
Andras Bacsai
059748ad3b fix: get service stack as uuid, not name 2024-02-11 15:44:02 +01:00
Andras Bacsai
a334f998a2 Add Bitnami Docker images for MariaDB, MongoDB, MySQL, PostgreSQL, and Redis 2024-02-11 15:40:02 +01:00
Andras Bacsai
53a5ccef31 fix: add docker compose check during server validation 2024-02-11 15:32:58 +01:00
Andras Bacsai
9eea73cefb Update Docker command in InstallLogDrain.php 2024-02-11 14:35:07 +01:00
Andras Bacsai
b210e1f243 fix: lock logdrain configuration when one of them are enabled 2024-02-11 14:31:21 +01:00
fipnooone
ef3202101c fix: flex wrap deployment previews 2024-02-10 13:40:35 +07:00
Varun Sahni
22d5159d16 Update directus-with-postgresql.yaml 2024-02-09 23:19:19 +05:30
Barnabás Schósz
1cbd30bd9e Fix Scheduled Tasks won't execute after deleting resource with scheduled task 2024-02-09 17:36:47 +01:00
Andras Bacsai
ad54358de7 Refactor resource index.blade.php file 2024-02-09 13:57:37 +01:00
Andras Bacsai
3689b58b92 Add success message for cleanup_queue 2024-02-09 13:51:31 +01:00
Andras Bacsai
5a4180a750 Update navbar dropdown menu styling 2024-02-09 13:50:19 +01:00
Andras Bacsai
047922b13a fix: new menu ui 2024-02-09 13:48:40 +01:00
Andras Bacsai
798d747164 add Docker run command parse test 2024-02-09 13:38:17 +01:00
Andras Bacsai
29676ffb22 Update Teams link in navbar.blade.php 2024-02-09 08:42:39 +01:00
Andras Bacsai
8a50b063d4 fix: user proper image_tag, if set 2024-02-08 15:22:07 +01:00
Andras Bacsai
3d2444ab2e Update version 4 to 4.0.0-beta.213 2024-02-08 14:54:30 +01:00
Andras Bacsai
7c395edab4 Fix conditional statement in navbar.blade.php 2024-02-08 14:06:43 +01:00
Andras Bacsai
7e7f322e21 Refactor admin authentication and routing***
***Add redirect for non-cloud users and instance admins without admin token.***

***Always include admin route, regardless of cloud status.
2024-02-08 14:01:16 +01:00
Andras Bacsai
9350fb4b97 Fix access control in Admin Index and hide Admin link in navbar 2024-02-08 13:54:16 +01:00
Andras Bacsai
59c3cc6ce1 Refactor admin authentication logic in Index component 2024-02-08 13:46:43 +01:00
Andras Bacsai
d7001937ac Fix access control in Admin Index and Navbar components 2024-02-08 13:40:26 +01:00
Andras Bacsai
3fe58ec66b Fix isInstanceAdmin function call in Index.php 2024-02-08 13:33:51 +01:00
Andras Bacsai
ed12f73483 Update admin authentication and version numbers 2024-02-08 13:33:34 +01:00
Andras Bacsai
6914280fb1 Merge pull request #1722 from coollabsio/next
v4.0.0-beta.212
2024-02-08 13:20:47 +01:00
Andras Bacsai
3dd5546369 Update destination.blade.php with server configurations 2024-02-08 13:19:11 +01:00
Andras Bacsai
576bff1af9 Remove exception and update server check in StopService 2024-02-08 13:17:08 +01:00
Andras Bacsai
3c4243d854 fix: go to prod env from dashboard if there is no other envs defined 2024-02-08 13:12:23 +01:00
Andras Bacsai
23d121d67a fix: make sure resources are deleted in async mode 2024-02-08 13:10:29 +01:00
Andras Bacsai
548304765c feat: cleanup queue 2024-02-08 12:47:00 +01:00
Andras Bacsai
037ba3ff79 Fix cleanup of halted deployments 2024-02-08 12:37:56 +01:00
Andras Bacsai
48b4c17391 Refactor init command to use full-cleanup option 2024-02-08 12:36:33 +01:00
Andras Bacsai
6acc0e6025 Add dynamic timeout for deployments 2024-02-08 12:34:01 +01:00
Andras Bacsai
43d7f746e4 Refactor destination.blade.php to include server and network information 2024-02-08 11:59:01 +01:00
Andras Bacsai
146fee14e5 Refactor destination.blade.php: Update server selection UI 2024-02-08 11:50:40 +01:00
Andras Bacsai
bde7fb2acb Fix user authentication condition in Index component 2024-02-08 11:46:23 +01:00
Andras Bacsai
08a729dc7b Add admin dashboard route and view 2024-02-08 11:45:19 +01:00
Andras Bacsai
7554de5993 Refactor app:init command and update cleanup options 2024-02-08 11:05:31 +01:00
Andras Bacsai
3d7295fec3 fix: new menu on navbar 2024-02-08 09:08:21 +01:00
Andras Bacsai
fd814abd8a Update version numbers to 4.0.0-beta.212 2024-02-07 20:44:17 +01:00
Andras Bacsai
4c38a59995 Merge branch 'main' into next 2024-02-07 20:35:35 +01:00
Andras Bacsai
43eb2fb00b new navbar 2024-02-07 15:31:03 +01:00
Andras Bacsai
9a899deeb8 Fix DNS validation and error handling 2024-02-07 14:59:33 +01:00
Andras Bacsai
9e1a7d5d9a feat: multi deployments 2024-02-07 14:55:06 +01:00
Andras Bacsai
5bdbab7276 ui: specific about newrelic logdrains 2024-02-07 09:04:35 +01:00
Andras Bacsai
13bceb934f Refactor Application model and migration 2024-02-06 17:37:07 +01:00
Andras Bacsai
78b194cb16 Refactor application status update logic and add complex_status column 2024-02-06 15:42:31 +01:00
Andras Bacsai
3616fc8ca9 Refactor code and add additional destinations 2024-02-06 15:05:11 +01:00
Andras Bacsai
10e307f92b Refactor help button in navbar and boarding layout 2024-02-06 11:50:03 +01:00
Andras Bacsai
01f027ac1b Update version numbers to 4.0.0-beta.211 2024-02-06 11:41:49 +01:00
RayBB
f357f40fc7 add syncthing template 2024-02-05 16:45:46 +01:00
131 changed files with 3316 additions and 1735 deletions

View File

@@ -10,6 +10,17 @@ No vendor lock-in, which means that all the configuration for your applications/
For more information, take a look at our landing page [here](https://coolify.io). For more information, take a look at our landing page [here](https://coolify.io).
# Installation
```bash
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
```
You can find the installation script source [here](./scripts/install.sh).
# Support
Contact us [here](https://coolify.io/docs/contact).
# Donations # Donations
To stay completely free, open-source, no feature behind paywall and evolve the project, we need your help. If you like Coolify, please consider donating to help us fund the future development of the project. To stay completely free, open-source, no feature behind paywall and evolve the project, we need your help. If you like Coolify, please consider donating to help us fund the future development of the project.
@@ -66,16 +77,6 @@ By subscribing to the cloud version, you get the Coolify server for the same pri
- Better support - Better support
- Less maintenance for you - Less maintenance for you
# Installation
```bash
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
```
You can find the installation script source [here](./scripts/install.sh).
# Support
Contact us [here](https://coolify.io/docs/contact).
# Recognitions # Recognitions

View File

@@ -3,6 +3,8 @@
namespace App\Actions\Application; namespace App\Actions\Application;
use App\Models\Application; use App\Models\Application;
use App\Models\StandaloneDocker;
use App\Notifications\Application\StatusChanged;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
class StopApplication class StopApplication
@@ -10,13 +12,20 @@ class StopApplication
use AsAction; use AsAction;
public function handle(Application $application) public function handle(Application $application)
{ {
$server = $application->destination->server; if ($application->destination->server->isSwarm()) {
if (!$server->isFunctional()) { instant_remote_process(["docker stack rm {$application->uuid}"], $application->destination->server);
return 'Server is not functional'; return;
} }
if ($server->isSwarm()) {
instant_remote_process(["docker stack rm {$application->uuid}" ], $server); $servers = collect([]);
} else { $servers->push($application->destination->server);
$application->additional_servers->map(function ($server) use ($servers) {
$servers->push($server);
});
foreach ($servers as $server) {
if (!$server->isFunctional()) {
return 'Server is not functional';
}
$containers = getCurrentApplicationContainerStatus($server, $application->id, 0); $containers = getCurrentApplicationContainerStatus($server, $application->id, 0);
if ($containers->count() > 0) { if ($containers->count() > 0) {
foreach ($containers as $container) { foreach ($containers as $container) {
@@ -28,20 +37,7 @@ class StopApplication
); );
} }
} }
// TODO: make notification for application
// $application->environment->project->team->notify(new StatusChanged($application));
}
// Delete Preview Deployments
$previewDeployments = $application->previews;
foreach ($previewDeployments as $previewDeployment) {
$containers = getCurrentApplicationContainerStatus($server, $application->id, $previewDeployment->pull_request_id);
foreach ($containers as $container) {
$name = str_replace('/', '', $container['Names']);
instant_remote_process(["docker rm -f $name"], $application->destination->server, throwError: false);
}
} }
} }
} }
} }

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Actions\Application;
use App\Models\Application;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
class StopApplicationOneServer
{
use AsAction;
public function handle(Application $application, Server $server)
{
if ($application->destination->server->isSwarm()) {
return;
}
if (!$server->isFunctional()) {
return 'Server is not functional';
}
try {
$containers = getCurrentApplicationContainerStatus($server, $application->id, 0);
if ($containers->count() > 0) {
foreach ($containers as $container) {
$containerName = data_get($container, 'Names');
if ($containerName) {
instant_remote_process(
["docker rm -f {$containerName}"],
$server
);
}
}
}
} catch (\Exception $e) {
ray($e->getMessage());
return $e->getMessage();
}
}
}

View File

@@ -106,7 +106,6 @@ class StartMariadb
$this->commands[] = "echo 'Pulling {$database->image} image.'"; $this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$database_name = addslashes($database->name);
$this->commands[] = "echo 'Database started.'"; $this->commands[] = "echo 'Database started.'";
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged'); return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
} }

View File

@@ -122,7 +122,6 @@ class StartMongodb
$this->commands[] = "echo 'Pulling {$database->image} image.'"; $this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$database_name = addslashes($database->name);
$this->commands[] = "echo 'Database started.'"; $this->commands[] = "echo 'Database started.'";
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged'); return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
} }

View File

@@ -106,7 +106,6 @@ class StartMysql
$this->commands[] = "echo 'Pulling {$database->image} image.'"; $this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$database_name = addslashes($database->name);
$this->commands[] = "echo 'Database started.'"; $this->commands[] = "echo 'Database started.'";
return remote_process($this->commands, $database->destination->server,callEventOnFinish: 'DatabaseStatusChanged'); return remote_process($this->commands, $database->destination->server,callEventOnFinish: 'DatabaseStatusChanged');
} }

View File

@@ -128,7 +128,6 @@ class StartPostgresql
$this->commands[] = "echo 'Pulling {$database->image} image.'"; $this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$database_name = addslashes($database->name);
$this->commands[] = "echo 'Database started.'"; $this->commands[] = "echo 'Database started.'";
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged'); return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
} }

View File

@@ -117,7 +117,6 @@ class StartRedis
$this->commands[] = "echo 'Pulling {$database->image} image.'"; $this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$database_name = addslashes($database->name);
$this->commands[] = "echo 'Database started.'"; $this->commands[] = "echo 'Database started.'";
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged'); return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
} }

View File

@@ -128,9 +128,9 @@ class InstallLogDrain
if ($type !== 'custom') { if ($type !== 'custom') {
$parsers = base64_encode(" $parsers = base64_encode("
[PARSER] [PARSER]
Name empty_line_skipper Name empty_line_skipper
Format regex Format regex
Regex /^(?!\s*$).+/ Regex /^(?!\s*$).+/
"); ");
} }
$compose = base64_encode(" $compose = base64_encode("
@@ -198,7 +198,7 @@ Files:
} }
$restart_command = [ $restart_command = [
"echo 'Stopping old Fluent Bit'", "echo 'Stopping old Fluent Bit'",
"cd $config_path && docker rm -f coolify-log-drain || true", "cd $config_path && docker compose down --remove-orphans || true",
"echo 'Starting Fluent Bit'", "echo 'Starting Fluent Bit'",
"cd $config_path && docker compose up -d --remove-orphans", "cd $config_path && docker compose up -d --remove-orphans",
]; ];

View File

@@ -25,7 +25,6 @@ class UpdateCoolify
CleanupDocker::run($this->server, false); CleanupDocker::run($this->server, false);
$this->latestVersion = get_latest_version_of_coolify(); $this->latestVersion = get_latest_version_of_coolify();
$this->currentVersion = config('version'); $this->currentVersion = config('version');
ray('latest version:' . $this->latestVersion . " current version: " . $this->currentVersion . ' force: ' . $force);
if ($settings->next_channel) { if ($settings->next_channel) {
ray('next channel enabled'); ray('next channel enabled');
$this->latestVersion = 'next'; $this->latestVersion = 'next';
@@ -44,7 +43,7 @@ class UpdateCoolify
} }
$this->update(); $this->update();
} }
send_internal_notification('InstanceAutoUpdateJob done to version: ' . $this->latestVersion . ' from version: ' . $this->currentVersion); send_internal_notification("Instance updated from {$this->currentVersion} -> {$this->latestVersion}");
} catch (\Throwable $e) { } catch (\Throwable $e) {
ray('InstanceAutoUpdateJob failed'); ray('InstanceAutoUpdateJob failed');
ray($e->getMessage()); ray($e->getMessage());

View File

@@ -45,6 +45,9 @@ class DeleteService
foreach ($service->databases()->get() as $database) { foreach ($service->databases()->get() as $database) {
$database->forceDelete(); $database->forceDelete();
} }
foreach ($service->scheduled_tasks as $task) {
$task->delete();
}
$service->tags()->detach(); $service->tags()->detach();
} }
} }

View File

@@ -13,10 +13,6 @@ class StartService
{ {
ray('Starting service: ' . $service->name); ray('Starting service: ' . $service->name);
$service->saveComposeConfigs(); $service->saveComposeConfigs();
$service_name = addslashes($service->name);
$server_name = addslashes($service->server->name);
$commands[] = "cd " . $service->workdir(); $commands[] = "cd " . $service->workdir();
$commands[] = "echo 'Saved configuration files to {$service->workdir()}.'"; $commands[] = "echo 'Saved configuration files to {$service->workdir()}.'";
$commands[] = "echo 'Creating Docker network.'"; $commands[] = "echo 'Creating Docker network.'";

View File

@@ -10,24 +10,31 @@ class StopService
use AsAction; use AsAction;
public function handle(Service $service) public function handle(Service $service)
{ {
$server = $service->destination->server; try {
if (!$server->isFunctional()) { $server = $service->destination->server;
return 'Server is not functional'; if (!$server->isFunctional()) {
return 'Server is not functional';
}
ray('Stopping service: ' . $service->name);
$applications = $service->applications()->get();
foreach ($applications as $application) {
instant_remote_process(["docker rm -f {$application->name}-{$service->uuid}"], $service->server);
$application->update(['status' => 'exited']);
}
$dbs = $service->databases()->get();
foreach ($dbs as $db) {
instant_remote_process(["docker rm -f {$db->name}-{$service->uuid}"], $service->server);
$db->update(['status' => 'exited']);
}
instant_remote_process(["docker network disconnect {$service->uuid} coolify-proxy 2>/dev/null"], $service->server, false);
instant_remote_process(["docker network rm {$service->uuid} 2>/dev/null"], $service->server, false);
// TODO: make notification for databases
// $service->environment->project->team->notify(new StatusChanged($service));
} catch (\Exception $e) {
echo $e->getMessage();
ray($e->getMessage());
return $e->getMessage();
} }
ray('Stopping service: ' . $service->name);
$applications = $service->applications()->get();
foreach ($applications as $application) {
instant_remote_process(["docker rm -f {$application->name}-{$service->uuid}"], $service->server);
$application->update(['status' => 'exited']);
}
$dbs = $service->databases()->get();
foreach ($dbs as $db) {
instant_remote_process(["docker rm -f {$db->name}-{$service->uuid}"], $service->server);
$db->update(['status' => 'exited']);
}
instant_remote_process(["docker network disconnect {$service->uuid} coolify-proxy 2>/dev/null"], $service->server, false);
instant_remote_process(["docker network rm {$service->uuid} 2>/dev/null"], $service->server, false);
// TODO: make notification for databases
// $service->environment->project->team->notify(new StatusChanged($service));
} }
} }

View File

@@ -0,0 +1,55 @@
<?php
namespace App\Actions\Shared;
use App\Models\Application;
use Lorisleiva\Actions\Concerns\AsAction;
class ComplexStatusCheck
{
use AsAction;
public function handle(Application $application)
{
$servers = $application->additional_servers;
$servers->push($application->destination->server);
foreach ($servers as $server) {
$is_main_server = $application->destination->server->id === $server->id;
if (!$server->isFunctional()) {
if ($is_main_server) {
$application->update(['status' => 'exited:unhealthy']);
continue;
} else {
$application->additional_servers()->updateExistingPivot($server->id, ['status' => 'exited:unhealthy']);
continue;
}
}
$container = instant_remote_process(["docker container inspect $(docker container ls -q --filter 'label=coolify.applicationId={$application->id}' --filter 'label=coolify.pullRequestId=0') --format '{{json .}}'"], $server, false);
$container = format_docker_command_output_to_json($container);
if ($container->count() === 1) {
$container = $container->first();
$containerStatus = data_get($container, 'State.Status');
$containerHealth = data_get($container, 'State.Health.Status', 'unhealthy');
if ($is_main_server) {
$statusFromDb = $application->status;
if ($statusFromDb !== $containerStatus) {
$application->update(['status' => "$containerStatus:$containerHealth"]);
}
} else {
$additional_server = $application->additional_servers()->wherePivot('server_id', $server->id);
$statusFromDb = $additional_server->first()->pivot->status;
if ($statusFromDb !== $containerStatus) {
$additional_server->updateExistingPivot($server->id, ['status' => "$containerStatus:$containerHealth"]);
}
}
} else {
if ($is_main_server) {
$application->update(['status' => 'exited:unhealthy']);
continue;
} else {
$application->additional_servers()->updateExistingPivot($server->id, ['status' => 'exited:unhealthy']);
continue;
}
}
}
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Models\Application; use App\Models\Application;
use App\Models\ScheduledTask;
use App\Models\Service; use App\Models\Service;
use App\Models\ServiceApplication; use App\Models\ServiceApplication;
use App\Models\ServiceDatabase; use App\Models\ServiceDatabase;
@@ -20,7 +21,8 @@ class CleanupStuckedResources extends Command
public function handle() public function handle()
{ {
echo "Running cleanup stucked...\n"; ray('Running cleanup stucked resources.');
echo "Running cleanup stucked resources.\n";
$this->cleanup_stucked_resources(); $this->cleanup_stucked_resources();
} }
private function cleanup_stucked_resources() private function cleanup_stucked_resources()
@@ -107,24 +109,35 @@ class CleanupStuckedResources extends Command
} catch (\Throwable $e) { } catch (\Throwable $e) {
echo "Error in cleaning stuck serviceapp: {$e->getMessage()}\n"; echo "Error in cleaning stuck serviceapp: {$e->getMessage()}\n";
} }
try {
$scheduled_tasks = ScheduledTask::all();
foreach ($scheduled_tasks as $scheduled_task) {
if (!$scheduled_task->service && !$scheduled_task->application) {
echo "Deleting stuck scheduledtask: {$scheduled_task->name}\n";
$scheduled_task->delete();
}
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck scheduledtasks: {$e->getMessage()}\n";
}
// Cleanup any resources that are not attached to any environment or destination or server // Cleanup any resources that are not attached to any environment or destination or server
try { try {
$applications = Application::all(); $applications = Application::all();
foreach ($applications as $application) { foreach ($applications as $application) {
if (!data_get($application, 'environment')) { if (!data_get($application, 'environment')) {
echo 'Application without environment: ' . $application->name . ' soft deleting\n'; echo 'Application without environment: ' . $application->name . '\n';
$application->delete(); $application->forceDelete();
continue; continue;
} }
if (!$application->destination()) { if (!$application->destination()) {
echo 'Application without destination: ' . $application->name . ' soft deleting\n'; echo 'Application without destination: ' . $application->name . '\n';
$application->delete(); $application->forceDelete();
continue; continue;
} }
if (!data_get($application, 'destination.server')) { if (!data_get($application, 'destination.server')) {
echo 'Application without server: ' . $application->name . ' soft deleting\n'; echo 'Application without server: ' . $application->name . '\n';
$application->delete(); $application->forceDelete();
continue; continue;
} }
} }
@@ -135,18 +148,18 @@ class CleanupStuckedResources extends Command
$postgresqls = StandalonePostgresql::all()->where('id', '!=', 0); $postgresqls = StandalonePostgresql::all()->where('id', '!=', 0);
foreach ($postgresqls as $postgresql) { foreach ($postgresqls as $postgresql) {
if (!data_get($postgresql, 'environment')) { if (!data_get($postgresql, 'environment')) {
echo 'Postgresql without environment: ' . $postgresql->name . ' soft deleting\n'; echo 'Postgresql without environment: ' . $postgresql->name . '\n';
$postgresql->delete(); $postgresql->forceDelete();
continue; continue;
} }
if (!$postgresql->destination()) { if (!$postgresql->destination()) {
echo 'Postgresql without destination: ' . $postgresql->name . ' soft deleting\n'; echo 'Postgresql without destination: ' . $postgresql->name . '\n';
$postgresql->delete(); $postgresql->forceDelete();
continue; continue;
} }
if (!data_get($postgresql, 'destination.server')) { if (!data_get($postgresql, 'destination.server')) {
echo 'Postgresql without server: ' . $postgresql->name . ' soft deleting\n'; echo 'Postgresql without server: ' . $postgresql->name . '\n';
$postgresql->delete(); $postgresql->forceDelete();
continue; continue;
} }
} }
@@ -157,18 +170,18 @@ class CleanupStuckedResources extends Command
$redis = StandaloneRedis::all(); $redis = StandaloneRedis::all();
foreach ($redis as $redis) { foreach ($redis as $redis) {
if (!data_get($redis, 'environment')) { if (!data_get($redis, 'environment')) {
echo 'Redis without environment: ' . $redis->name . ' soft deleting\n'; echo 'Redis without environment: ' . $redis->name . '\n';
$redis->delete(); $redis->forceDelete();
continue; continue;
} }
if (!$redis->destination()) { if (!$redis->destination()) {
echo 'Redis without destination: ' . $redis->name . ' soft deleting\n'; echo 'Redis without destination: ' . $redis->name . '\n';
$redis->delete(); $redis->forceDelete();
continue; continue;
} }
if (!data_get($redis, 'destination.server')) { if (!data_get($redis, 'destination.server')) {
echo 'Redis without server: ' . $redis->name . ' soft deleting\n'; echo 'Redis without server: ' . $redis->name . '\n';
$redis->delete(); $redis->forceDelete();
continue; continue;
} }
} }
@@ -180,18 +193,18 @@ class CleanupStuckedResources extends Command
$mongodbs = StandaloneMongodb::all(); $mongodbs = StandaloneMongodb::all();
foreach ($mongodbs as $mongodb) { foreach ($mongodbs as $mongodb) {
if (!data_get($mongodb, 'environment')) { if (!data_get($mongodb, 'environment')) {
echo 'Mongodb without environment: ' . $mongodb->name . ' soft deleting\n'; echo 'Mongodb without environment: ' . $mongodb->name . '\n';
$mongodb->delete(); $mongodb->forceDelete();
continue; continue;
} }
if (!$mongodb->destination()) { if (!$mongodb->destination()) {
echo 'Mongodb without destination: ' . $mongodb->name . ' soft deleting\n'; echo 'Mongodb without destination: ' . $mongodb->name . '\n';
$mongodb->delete(); $mongodb->forceDelete();
continue; continue;
} }
if (!data_get($mongodb, 'destination.server')) { if (!data_get($mongodb, 'destination.server')) {
echo 'Mongodb without server: ' . $mongodb->name . ' soft deleting\n'; echo 'Mongodb without server: ' . $mongodb->name . '\n';
$mongodb->delete(); $mongodb->forceDelete();
continue; continue;
} }
} }
@@ -203,18 +216,18 @@ class CleanupStuckedResources extends Command
$mysqls = StandaloneMysql::all(); $mysqls = StandaloneMysql::all();
foreach ($mysqls as $mysql) { foreach ($mysqls as $mysql) {
if (!data_get($mysql, 'environment')) { if (!data_get($mysql, 'environment')) {
echo 'Mysql without environment: ' . $mysql->name . ' soft deleting\n'; echo 'Mysql without environment: ' . $mysql->name . '\n';
$mysql->delete(); $mysql->forceDelete();
continue; continue;
} }
if (!$mysql->destination()) { if (!$mysql->destination()) {
echo 'Mysql without destination: ' . $mysql->name . ' soft deleting\n'; echo 'Mysql without destination: ' . $mysql->name . '\n';
$mysql->delete(); $mysql->forceDelete();
continue; continue;
} }
if (!data_get($mysql, 'destination.server')) { if (!data_get($mysql, 'destination.server')) {
echo 'Mysql without server: ' . $mysql->name . ' soft deleting\n'; echo 'Mysql without server: ' . $mysql->name . '\n';
$mysql->delete(); $mysql->forceDelete();
continue; continue;
} }
} }
@@ -226,18 +239,18 @@ class CleanupStuckedResources extends Command
$mariadbs = StandaloneMariadb::all(); $mariadbs = StandaloneMariadb::all();
foreach ($mariadbs as $mariadb) { foreach ($mariadbs as $mariadb) {
if (!data_get($mariadb, 'environment')) { if (!data_get($mariadb, 'environment')) {
echo 'Mariadb without environment: ' . $mariadb->name . ' soft deleting\n'; echo 'Mariadb without environment: ' . $mariadb->name . '\n';
$mariadb->delete(); $mariadb->forceDelete();
continue; continue;
} }
if (!$mariadb->destination()) { if (!$mariadb->destination()) {
echo 'Mariadb without destination: ' . $mariadb->name . ' soft deleting\n'; echo 'Mariadb without destination: ' . $mariadb->name . '\n';
$mariadb->delete(); $mariadb->forceDelete();
continue; continue;
} }
if (!data_get($mariadb, 'destination.server')) { if (!data_get($mariadb, 'destination.server')) {
echo 'Mariadb without server: ' . $mariadb->name . ' soft deleting\n'; echo 'Mariadb without server: ' . $mariadb->name . '\n';
$mariadb->delete(); $mariadb->forceDelete();
continue; continue;
} }
} }
@@ -249,18 +262,18 @@ class CleanupStuckedResources extends Command
$services = Service::all(); $services = Service::all();
foreach ($services as $service) { foreach ($services as $service) {
if (!data_get($service, 'environment')) { if (!data_get($service, 'environment')) {
echo 'Service without environment: ' . $service->name . ' soft deleting\n'; echo 'Service without environment: ' . $service->name . '\n';
$service->delete(); $service->forceDelete();
continue; continue;
} }
if (!$service->destination()) { if (!$service->destination()) {
echo 'Service without destination: ' . $service->name . ' soft deleting\n'; echo 'Service without destination: ' . $service->name . '\n';
$service->delete(); $service->forceDelete();
continue; continue;
} }
if (!data_get($service, 'server')) { if (!data_get($service, 'server')) {
echo 'Service without server: ' . $service->name . ' soft deleting\n'; echo 'Service without server: ' . $service->name . '\n';
$service->delete(); $service->forceDelete();
continue; continue;
} }
} }
@@ -271,8 +284,8 @@ class CleanupStuckedResources extends Command
$serviceApplications = ServiceApplication::all(); $serviceApplications = ServiceApplication::all();
foreach ($serviceApplications as $service) { foreach ($serviceApplications as $service) {
if (!data_get($service, 'service')) { if (!data_get($service, 'service')) {
echo 'ServiceApplication without service: ' . $service->name . ' soft deleting\n'; echo 'ServiceApplication without service: ' . $service->name . '\n';
$service->delete(); $service->forceDelete();
continue; continue;
} }
} }
@@ -283,8 +296,8 @@ class CleanupStuckedResources extends Command
$serviceDatabases = ServiceDatabase::all(); $serviceDatabases = ServiceDatabase::all();
foreach ($serviceDatabases as $service) { foreach ($serviceDatabases as $service) {
if (!data_get($service, 'service')) { if (!data_get($service, 'service')) {
echo 'ServiceDatabase without service: ' . $service->name . ' soft deleting\n'; echo 'ServiceDatabase without service: ' . $service->name . '\n';
$service->delete(); $service->forceDelete();
continue; continue;
} }
} }

View File

@@ -14,39 +14,44 @@ use Illuminate\Support\Facades\Http;
class Init extends Command class Init extends Command
{ {
protected $signature = 'app:init {--cleanup}'; protected $signature = 'app:init {--full-cleanup} {--cleanup-deployments}';
protected $description = 'Cleanup instance related stuffs'; protected $description = 'Cleanup instance related stuffs';
public function handle() public function handle()
{ {
$this->alive(); $this->alive();
$cleanup = $this->option('cleanup'); $full_cleanup = $this->option('full-cleanup');
if ($cleanup) { $cleanup_deployments = $this->option('cleanup-deployments');
echo "Running cleanups...\n"; if ($cleanup_deployments) {
$this->call('cleanup:stucked-resources'); echo "Running cleanup deployments.\n";
$this->cleanup_in_progress_application_deployments();
return;
}
if ($full_cleanup) {
// Required for falsely deleted coolify db // Required for falsely deleted coolify db
$this->restore_coolify_db_backup(); $this->restore_coolify_db_backup();
$this->cleanup_in_progress_application_deployments();
// $this->cleanup_ssh(); $this->cleanup_stucked_helper_containers();
} $this->call('cleanup:queue');
$this->cleanup_in_progress_application_deployments(); $this->call('cleanup:stucked-resources');
$this->cleanup_stucked_helper_containers(); try {
setup_dynamic_configuration();
try { } catch (\Throwable $e) {
setup_dynamic_configuration(); echo "Could not setup dynamic configuration: {$e->getMessage()}\n";
} catch (\Throwable $e) {
echo "Could not setup dynamic configuration: {$e->getMessage()}\n";
}
$settings = InstanceSettings::get();
if (!is_null(env('AUTOUPDATE', null))) {
if (env('AUTOUPDATE') == true) {
$settings->update(['is_auto_update_enabled' => true]);
} else {
$settings->update(['is_auto_update_enabled' => false]);
} }
$settings = InstanceSettings::get();
if (!is_null(env('AUTOUPDATE', null))) {
if (env('AUTOUPDATE') == true) {
$settings->update(['is_auto_update_enabled' => true]);
} else {
$settings->update(['is_auto_update_enabled' => false]);
}
}
return;
} }
$this->call('cleanup:queue'); $this->cleanup_stucked_helper_containers();
$this->call('cleanup:stucked-resources');
} }
private function restore_coolify_db_backup() private function restore_coolify_db_backup()
{ {
@@ -120,8 +125,13 @@ class Init extends Command
// Cleanup any failed deployments // Cleanup any failed deployments
try { try {
$halted_deployments = ApplicationDeploymentQueue::where('status', '==', ApplicationDeploymentStatus::IN_PROGRESS)->where('status', '==', ApplicationDeploymentStatus::QUEUED)->get(); if (isCloud()) {
foreach ($halted_deployments as $deployment) { return;
}
$queued_inprogress_deployments = ApplicationDeploymentQueue::whereIn('status', [ApplicationDeploymentStatus::IN_PROGRESS->value, ApplicationDeploymentStatus::QUEUED->value])->get();
foreach ($queued_inprogress_deployments as $deployment) {
ray($deployment->id, $deployment->status);
echo "Cleaning up deployment: {$deployment->id}\n";
$deployment->status = ApplicationDeploymentStatus::FAILED->value; $deployment->status = ApplicationDeploymentStatus::FAILED->value;
$deployment->save(); $deployment->save();
} }
@@ -129,5 +139,4 @@ class Init extends Command
echo "Error: {$e->getMessage()}\n"; echo "Error: {$e->getMessage()}\n";
} }
} }
} }

View File

@@ -4,6 +4,7 @@ namespace App\Console;
use App\Jobs\CheckLogDrainContainerJob; use App\Jobs\CheckLogDrainContainerJob;
use App\Jobs\CleanupInstanceStuffsJob; use App\Jobs\CleanupInstanceStuffsJob;
use App\Jobs\ComplexContainerStatusJob;
use App\Jobs\DatabaseBackupJob; use App\Jobs\DatabaseBackupJob;
use App\Jobs\ScheduledTaskJob; use App\Jobs\ScheduledTaskJob;
use App\Jobs\InstanceAutoUpdateJob; use App\Jobs\InstanceAutoUpdateJob;
@@ -91,7 +92,6 @@ class Kernel extends ConsoleKernel
{ {
$scheduled_backups = ScheduledDatabaseBackup::all(); $scheduled_backups = ScheduledDatabaseBackup::all();
if ($scheduled_backups->isEmpty()) { if ($scheduled_backups->isEmpty()) {
ray('no scheduled backups');
return; return;
} }
foreach ($scheduled_backups as $scheduled_backup) { foreach ($scheduled_backups as $scheduled_backup) {
@@ -117,12 +117,11 @@ class Kernel extends ConsoleKernel
{ {
$scheduled_tasks = ScheduledTask::all(); $scheduled_tasks = ScheduledTask::all();
if ($scheduled_tasks->isEmpty()) { if ($scheduled_tasks->isEmpty()) {
ray('no scheduled tasks');
return; return;
} }
foreach ($scheduled_tasks as $scheduled_task) { foreach ($scheduled_tasks as $scheduled_task) {
$service = $scheduled_task->service()->get(); $service = $scheduled_task->service;
$application = $scheduled_task->application()->get(); $application = $scheduled_task->application;
if (!$application && !$service) { if (!$application && !$service) {
ray('application/service attached to scheduled task does not exist'); ray('application/service attached to scheduled task does not exist');

View File

@@ -18,8 +18,7 @@ class Deploy extends Controller
{ {
public function deploy(Request $request) public function deploy(Request $request)
{ {
$token = auth()->user()->currentAccessToken(); $teamId = get_team_id_from_token();
$teamId = data_get($token, 'team_id');
$uuids = $request->query->get('uuid'); $uuids = $request->query->get('uuid');
$tags = $request->query->get('tag'); $tags = $request->query->get('tag');
$force = $request->query->get('force') ?? false; $force = $request->query->get('force') ?? false;

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Project as ModelsProject;
use Illuminate\Http\Request;
class Project extends Controller
{
public function projects(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return response()->json(['error' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api/authentication'], 400);
}
$projects = ModelsProject::whereTeamId($teamId)->select('id', 'name', 'uuid')->get();
return response()->json($projects);
}
public function project_by_uuid(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return response()->json(['error' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api/authentication'], 400);
}
$project = ModelsProject::whereTeamId($teamId)->whereUuid(request()->uuid)->first()->load(['environments']);
return response()->json($project);
}
public function environment_details(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return response()->json(['error' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api/authentication'], 400);
}
$project = ModelsProject::whereTeamId($teamId)->whereUuid(request()->uuid)->first();
$environment = $project->environments()->whereName(request()->environment_name)->first()->load(['applications', 'postgresqls', 'redis', 'mongodbs', 'mysqls', 'mariadbs', 'services']);
return response()->json($environment);
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Server as ModelsServer;
use Illuminate\Http\Request;
class Server extends Controller
{
public function servers(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return response()->json(['error' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api/authentication'], 400);
}
$servers = ModelsServer::whereTeamId($teamId)->select('id', 'name', 'uuid', 'ip', 'user', 'port')->get()->load(['settings'])->map(function ($server) {
$server['is_reachable'] = $server->settings->is_reachable;
$server['is_usable'] = $server->settings->is_usable;
return $server;
});
ray($servers);
return response()->json($servers);
}
public function server_by_uuid(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return response()->json(['error' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api/authentication'], 400);
}
$server = ModelsServer::whereTeamId($teamId)->whereUuid(request()->uuid)->first();
if (is_null($server)) {
return response()->json(['error' => 'Server not found.'], 404);
}
$server->load(['settings']);
$server['resources'] = $server->definedResources()->map(function ($resource) {
$payload = [
'id' => $resource->id,
'uuid' => $resource->uuid,
'name' => $resource->name,
'type' => $resource->type(),
'created_at' => $resource->created_at,
'updated_at' => $resource->updated_at,
];
if ($resource->type() === 'service') {
$payload['status'] = $resource->status();
} else {
$payload['status'] = $resource->status;
}
return $payload;
});
return response()->json($server);
}
}

View File

@@ -123,6 +123,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->source = $source->getMorphClass()::where('id', $this->application->source->id)->first(); $this->source = $source->getMorphClass()::where('id', $this->application->source->id)->first();
} }
$this->server = Server::find($this->application_deployment_queue->server_id); $this->server = Server::find($this->application_deployment_queue->server_id);
$this->timeout = $this->server->settings->dynamic_timeout;
$this->destination = $this->server->destinations()->where('id', $this->application_deployment_queue->destination_id)->first(); $this->destination = $this->server->destinations()->where('id', $this->application_deployment_queue->destination_id)->first();
$this->server = $this->mainServer = $this->destination->server; $this->server = $this->mainServer = $this->destination->server;
$this->serverUser = $this->server->user; $this->serverUser = $this->server->user;
@@ -132,6 +133,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->is_debug_enabled = $this->application->settings->is_debug_enabled; $this->is_debug_enabled = $this->application->settings->is_debug_enabled;
$this->container_name = generateApplicationContainerName($this->application, $this->pull_request_id); $this->container_name = generateApplicationContainerName($this->application, $this->pull_request_id);
ray('New container name: ', $this->container_name);
savePrivateKeyToFs($this->server); savePrivateKeyToFs($this->server);
$this->saved_outputs = collect(); $this->saved_outputs = collect();
@@ -248,9 +251,14 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
dispatch(new ContainerStatusJob($this->server)); dispatch(new ContainerStatusJob($this->server));
} }
// Otherwise built image needs to be pushed before from the build server. // Otherwise built image needs to be pushed before from the build server.
if (!$this->use_build_server) { // ray($this->use_build_server);
$this->push_to_docker_registry(); // if (!$this->use_build_server) {
} // if ($this->application->additional_servers->count() > 0) {
// $this->push_to_docker_registry(forceFail: true);
// } else {
// $this->push_to_docker_registry();
// }
// }
$this->next(ApplicationDeploymentStatus::FINISHED->value); $this->next(ApplicationDeploymentStatus::FINISHED->value);
if ($this->pull_request_id !== 0) { if ($this->pull_request_id !== 0) {
if ($this->application->is_github_based()) { if ($this->application->is_github_based()) {
@@ -288,162 +296,13 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
ApplicationStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id')); ApplicationStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id'));
} }
} }
private function write_deployment_configurations()
{
if (isset($this->docker_compose_base64)) {
if ($this->use_build_server) {
$this->server = $this->original_server;
}
$readme = generate_readme_file($this->application->name, $this->application_deployment_queue->updated_at);
$composeFileName = "$this->configuration_dir/docker-compose.yml";
if ($this->pull_request_id !== 0) {
$composeFileName = "$this->configuration_dir/docker-compose-pr-{$this->pull_request_id}.yml";
}
$this->execute_remote_command(
[
"mkdir -p $this->configuration_dir"
],
[
"echo '{$this->docker_compose_base64}' | base64 -d > $composeFileName",
],
[
"echo '{$readme}' > $this->configuration_dir/README.md",
]
);
if ($this->use_build_server) {
$this->server = $this->build_server;
}
}
}
private function push_to_docker_registry($forceFail = false)
{
if (
$this->application->docker_registry_image_name &&
$this->application->build_pack !== 'dockerimage' &&
!$this->application->destination->server->isSwarm() &&
!$this->restart_only &&
!(str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged())
) {
try {
instant_remote_process(["docker images --format '{{json .}}' {$this->production_image_name}"], $this->server);
$this->application_deployment_queue->addLogEntry("----------------------------------------");
$this->application_deployment_queue->addLogEntry("Pushing image to docker registry ({$this->production_image_name}).");
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "docker push {$this->production_image_name}"), 'hidden' => true
],
);
if ($this->application->docker_registry_image_tag) {
// Tag image with latest
$this->application_deployment_queue->addLogEntry("Tagging and pushing image with latest tag.");
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "docker tag {$this->production_image_name} {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true
],
[
executeInDocker($this->deployment_uuid, "docker push {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true
],
);
}
$this->application_deployment_queue->addLogEntry("Image pushed to docker registry.");
} catch (Exception $e) {
$this->application_deployment_queue->addLogEntry("Failed to push image to docker registry. Please check debug logs for more information.");
if ($forceFail) {
throw $e;
}
ray($e);
}
}
}
private function generate_image_names()
{
if ($this->application->dockerfile) {
if ($this->application->docker_registry_image_name) {
$this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:build");
$this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:latest");
} else {
$this->build_image_name = Str::lower("{$this->application->uuid}:build");
$this->production_image_name = Str::lower("{$this->application->uuid}:latest");
}
} else if ($this->application->build_pack === 'dockerimage') {
$this->production_image_name = Str::lower("{$this->dockerImage}:{$this->dockerImageTag}");
} else if ($this->pull_request_id !== 0) {
if ($this->application->docker_registry_image_name) {
$this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:pr-{$this->pull_request_id}-build");
$this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:pr-{$this->pull_request_id}");
} else {
$this->build_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}-build");
$this->production_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}");
}
} else {
$this->dockerImageTag = str($this->commit)->substr(0, 128);
if ($this->application->docker_registry_image_name) {
$this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:{$this->dockerImageTag}-build");
$this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:{$this->dockerImageTag}");
} else {
$this->build_image_name = Str::lower("{$this->application->uuid}:{$this->dockerImageTag}-build");
$this->production_image_name = Str::lower("{$this->application->uuid}:{$this->dockerImageTag}");
}
}
}
private function just_restart()
{
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch}.");
$this->prepare_builder_image();
$this->check_git_if_build_needed();
$this->set_base_dir();
$this->generate_image_names();
$this->check_image_locally_or_remotely();
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty()) {
$this->application_deployment_queue->addLogEntry("Image found ({$this->production_image_name}) with the same Git Commit SHA. Restarting container.");
$this->create_workdir();
$this->generate_compose_file();
$this->rolling_update();
return;
}
throw new RuntimeException('Cannot find image anywhere. Please redeploy the application.');
}
private function check_image_locally_or_remotely()
{
$this->execute_remote_command([
"docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found"
]);
if (str($this->saved_outputs->get('local_image_found'))->isEmpty() && $this->application->docker_registry_image_name) {
$this->execute_remote_command([
"docker pull {$this->production_image_name} 2>/dev/null", "ignore_errors" => true, "hidden" => true
]);
$this->execute_remote_command([
"docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found"
]);
}
}
private function save_environment_variables()
{
$envs = collect([]);
if ($this->pull_request_id !== 0) {
foreach ($this->application->environment_variables_preview as $env) {
$envs->push($env->key . '=' . $env->real_value);
}
} else {
foreach ($this->application->environment_variables as $env) {
$envs->push($env->key . '=' . $env->real_value);
}
}
$envs_base64 = base64_encode($envs->implode("\n"));
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d > $this->workdir/.env")
],
);
}
private function deploy_simple_dockerfile() private function deploy_simple_dockerfile()
{ {
if ($this->use_build_server) { if ($this->use_build_server) {
$this->server = $this->build_server; $this->server = $this->build_server;
} }
$dockerfile_base64 = base64_encode($this->application->dockerfile); $dockerfile_base64 = base64_encode($this->application->dockerfile);
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->application->name}."); $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->application->name} to {$this->server->name}.");
$this->prepare_builder_image(); $this->prepare_builder_image();
$this->execute_remote_command( $this->execute_remote_command(
[ [
@@ -451,19 +310,30 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
], ],
); );
$this->generate_image_names(); $this->generate_image_names();
if (!$this->force_rebuild) {
$this->check_image_locally_or_remotely();
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
$this->create_workdir();
$this->application_deployment_queue->addLogEntry("No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.");
$this->generate_compose_file();
$this->push_to_docker_registry();
$this->rolling_update();
return;
}
}
$this->generate_compose_file(); $this->generate_compose_file();
$this->generate_build_env_variables(); $this->generate_build_env_variables();
$this->add_build_env_variables_to_dockerfile(); $this->add_build_env_variables_to_dockerfile();
$this->build_image(); $this->build_image();
$this->push_to_docker_registry();
$this->rolling_update(); $this->rolling_update();
} }
private function deploy_dockerimage_buildpack() private function deploy_dockerimage_buildpack()
{ {
$this->dockerImage = $this->application->docker_registry_image_name; $this->dockerImage = $this->application->docker_registry_image_name;
$this->dockerImageTag = $this->application->docker_registry_image_tag; $this->dockerImageTag = $this->application->docker_registry_image_tag;
ray("echo 'Starting deployment of {$this->dockerImage}:{$this->dockerImageTag}.'"); ray("echo 'Starting deployment of {$this->dockerImage}:{$this->dockerImageTag} to {$this->server->name}.'");
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->dockerImage}:{$this->dockerImageTag}."); $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->dockerImage}:{$this->dockerImageTag} to {$this->server->name}.");
$this->generate_image_names(); $this->generate_image_names();
$this->prepare_builder_image(); $this->prepare_builder_image();
$this->generate_compose_file(); $this->generate_compose_file();
@@ -481,9 +351,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->docker_compose_custom_build_command = $this->application->docker_compose_custom_build_command; $this->docker_compose_custom_build_command = $this->application->docker_compose_custom_build_command;
} }
if ($this->pull_request_id === 0) { if ($this->pull_request_id === 0) {
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->application->name}."); $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->application->name} to {$this->server->name}.");
} else { } else {
$this->application_deployment_queue->addLogEntry("Starting pull request (#{$this->pull_request_id}) deployment of {$this->customRepository}:{$this->application->git_branch}."); $this->application_deployment_queue->addLogEntry("Starting pull request (#{$this->pull_request_id}) deployment of {$this->customRepository}:{$this->application->git_branch} to {$this->server->name}.");
} }
$this->prepare_builder_image(); $this->prepare_builder_image();
$this->check_git_if_build_needed(); $this->check_git_if_build_needed();
@@ -551,25 +421,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if (data_get($this->application, 'dockerfile_location')) { if (data_get($this->application, 'dockerfile_location')) {
$this->dockerfile_location = $this->application->dockerfile_location; $this->dockerfile_location = $this->application->dockerfile_location;
} }
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch}."); $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch} to {$this->server->name}.");
$this->prepare_builder_image();
$this->check_git_if_build_needed();
$this->clone_repository();
$this->set_base_dir();
$this->generate_image_names();
$this->cleanup_git();
$this->generate_compose_file();
$this->generate_build_env_variables();
$this->add_build_env_variables_to_dockerfile();
$this->build_image();
$this->rolling_update();
}
private function deploy_nixpacks_buildpack()
{
if ($this->use_build_server) {
$this->server = $this->build_server;
}
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch}.");
$this->prepare_builder_image(); $this->prepare_builder_image();
$this->check_git_if_build_needed(); $this->check_git_if_build_needed();
$this->set_base_dir(); $this->set_base_dir();
@@ -580,6 +432,38 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->create_workdir(); $this->create_workdir();
$this->application_deployment_queue->addLogEntry("No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped."); $this->application_deployment_queue->addLogEntry("No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.");
$this->generate_compose_file(); $this->generate_compose_file();
$this->push_to_docker_registry();
$this->rolling_update();
return;
}
}
$this->clone_repository();
$this->cleanup_git();
$this->generate_compose_file();
$this->generate_build_env_variables();
$this->add_build_env_variables_to_dockerfile();
$this->build_image();
$this->push_to_docker_registry();
$this->rolling_update();
}
private function deploy_nixpacks_buildpack()
{
if ($this->use_build_server) {
$this->server = $this->build_server;
}
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch} to {$this->server->name}.");
$this->prepare_builder_image();
$this->check_git_if_build_needed();
$this->set_base_dir();
$this->generate_image_names();
if (!$this->force_rebuild) {
$this->check_image_locally_or_remotely();
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
$this->create_workdir();
$this->application_deployment_queue->addLogEntry("No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.");
$this->generate_compose_file();
ray('pushing to docker registry');
$this->push_to_docker_registry();
$this->rolling_update(); $this->rolling_update();
return; return;
} }
@@ -592,8 +476,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->generate_nixpacks_confs(); $this->generate_nixpacks_confs();
$this->generate_compose_file(); $this->generate_compose_file();
$this->generate_build_env_variables(); $this->generate_build_env_variables();
// $this->add_build_env_variables_to_dockerfile();
$this->build_image(); $this->build_image();
$this->push_to_docker_registry();
$this->rolling_update(); $this->rolling_update();
} }
private function deploy_static_buildpack() private function deploy_static_buildpack()
@@ -601,18 +485,205 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($this->use_build_server) { if ($this->use_build_server) {
$this->server = $this->build_server; $this->server = $this->build_server;
} }
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch}."); $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch} to {$this->server->name}.");
$this->prepare_builder_image(); $this->prepare_builder_image();
$this->check_git_if_build_needed(); $this->check_git_if_build_needed();
$this->set_base_dir(); $this->set_base_dir();
$this->generate_image_names(); $this->generate_image_names();
if (!$this->force_rebuild) {
$this->check_image_locally_or_remotely();
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
$this->create_workdir();
$this->application_deployment_queue->addLogEntry("No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.");
$this->generate_compose_file();
$this->push_to_docker_registry();
$this->rolling_update();
return;
}
}
$this->clone_repository(); $this->clone_repository();
$this->cleanup_git(); $this->cleanup_git();
$this->build_image();
$this->generate_compose_file(); $this->generate_compose_file();
$this->build_image();
$this->push_to_docker_registry();
$this->rolling_update(); $this->rolling_update();
} }
private function write_deployment_configurations()
{
if (isset($this->docker_compose_base64)) {
if ($this->use_build_server) {
$this->server = $this->original_server;
}
$readme = generate_readme_file($this->application->name, $this->application_deployment_queue->updated_at);
$composeFileName = "$this->configuration_dir/docker-compose.yml";
if ($this->pull_request_id !== 0) {
$composeFileName = "$this->configuration_dir/docker-compose-pr-{$this->pull_request_id}.yml";
}
$this->execute_remote_command(
[
"mkdir -p $this->configuration_dir"
],
[
"echo '{$this->docker_compose_base64}' | base64 -d > $composeFileName",
],
[
"echo '{$readme}' > $this->configuration_dir/README.md",
]
);
if ($this->use_build_server) {
$this->server = $this->build_server;
}
}
}
private function push_to_docker_registry()
{
$forceFail = true;
if (str($this->application->docker_registry_image_name)->isEmpty()) {
ray('empty docker_registry_image_name');
return;
}
if ($this->restart_only) {
ray('restart_only');
return;
}
if ($this->application->build_pack === 'dockerimage') {
ray('dockerimage');
return;
}
if ($this->use_build_server) {
ray('use_build_server');
$forceFail = true;
}
if ($this->server->isSwarm() && $this->build_pack !== 'dockerimage') {
ray('isSwarm');
$forceFail = true;
}
if ($this->application->additional_servers->count() > 0) {
ray('additional_servers');
$forceFail = true;
}
if ($this->application->additional_servers()->wherePivot('server_id', $this->server->id)->count() > 0) {
ray('this is an additional_servers, no pushy pushy');
return;
}
ray('push_to_docker_registry noww: ' . $this->production_image_name);
try {
instant_remote_process(["docker images --format '{{json .}}' {$this->production_image_name}"], $this->server);
$this->application_deployment_queue->addLogEntry("----------------------------------------");
$this->application_deployment_queue->addLogEntry("Pushing image to docker registry ({$this->production_image_name}).");
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "docker push {$this->production_image_name}"), 'hidden' => true
],
);
if ($this->application->docker_registry_image_tag) {
// Tag image with latest
$this->application_deployment_queue->addLogEntry("Tagging and pushing image with latest tag.");
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "docker tag {$this->production_image_name} {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true
],
[
executeInDocker($this->deployment_uuid, "docker push {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true
],
);
}
$this->application_deployment_queue->addLogEntry("Image pushed to docker registry.");
} catch (Exception $e) {
$this->application_deployment_queue->addLogEntry("Failed to push image to docker registry. Please check debug logs for more information.");
if ($forceFail) {
throw new RuntimeException($e->getMessage(), 69420);
}
ray($e);
}
}
private function generate_image_names()
{
if ($this->application->dockerfile) {
if ($this->application->docker_registry_image_name) {
$this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:build");
$this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:latest");
} else {
$this->build_image_name = Str::lower("{$this->application->uuid}:build");
$this->production_image_name = Str::lower("{$this->application->uuid}:latest");
}
} else if ($this->application->build_pack === 'dockerimage') {
$this->production_image_name = Str::lower("{$this->dockerImage}:{$this->dockerImageTag}");
} else if ($this->pull_request_id !== 0) {
if ($this->application->docker_registry_image_name) {
$this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:pr-{$this->pull_request_id}-build");
$this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:pr-{$this->pull_request_id}");
} else {
$this->build_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}-build");
$this->production_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}");
}
} else {
$this->dockerImageTag = str($this->commit)->substr(0, 128);
if ($this->application->docker_registry_image_tag) {
$this->dockerImageTag = $this->application->docker_registry_image_tag;
}
if ($this->application->docker_registry_image_name) {
$this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:{$this->dockerImageTag}-build");
$this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:{$this->dockerImageTag}");
} else {
$this->build_image_name = Str::lower("{$this->application->uuid}:{$this->dockerImageTag}-build");
$this->production_image_name = Str::lower("{$this->application->uuid}:{$this->dockerImageTag}");
}
}
}
private function just_restart()
{
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch} to {$this->server->name}.");
$this->prepare_builder_image();
$this->check_git_if_build_needed();
$this->set_base_dir();
$this->generate_image_names();
$this->check_image_locally_or_remotely();
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty()) {
$this->application_deployment_queue->addLogEntry("Image found ({$this->production_image_name}) with the same Git Commit SHA. Restarting container.");
$this->create_workdir();
$this->generate_compose_file();
$this->rolling_update();
return;
}
throw new RuntimeException('Cannot find image anywhere. Please redeploy the application.');
}
private function check_image_locally_or_remotely()
{
$this->execute_remote_command([
"docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found"
]);
if (str($this->saved_outputs->get('local_image_found'))->isEmpty() && $this->application->docker_registry_image_name) {
$this->execute_remote_command([
"docker pull {$this->production_image_name} 2>/dev/null", "ignore_errors" => true, "hidden" => true
]);
$this->execute_remote_command([
"docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found"
]);
}
}
private function save_environment_variables()
{
$envs = collect([]);
if ($this->pull_request_id !== 0) {
foreach ($this->application->environment_variables_preview as $env) {
$envs->push($env->key . '=' . $env->real_value);
}
} else {
foreach ($this->application->environment_variables as $env) {
$envs->push($env->key . '=' . $env->real_value);
}
}
$envs_base64 = base64_encode($envs->implode("\n"));
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d > $this->workdir/.env")
],
);
}
private function framework_based_notification() private function framework_based_notification()
{ {
// Laravel old env variables // Laravel old env variables
@@ -630,9 +701,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function rolling_update() private function rolling_update()
{ {
if ($this->server->isSwarm()) { if ($this->server->isSwarm()) {
if ($this->build_pack !== 'dockerimage') {
$this->push_to_docker_registry(forceFail: true);
}
$this->application_deployment_queue->addLogEntry("Rolling update started."); $this->application_deployment_queue->addLogEntry("Rolling update started.");
$this->execute_remote_command( $this->execute_remote_command(
[ [
@@ -642,13 +710,17 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->application_deployment_queue->addLogEntry("Rolling update completed."); $this->application_deployment_queue->addLogEntry("Rolling update completed.");
} else { } else {
if ($this->use_build_server) { if ($this->use_build_server) {
$this->push_to_docker_registry(forceFail: true);
$this->write_deployment_configurations(); $this->write_deployment_configurations();
$this->server = $this->original_server; $this->server = $this->original_server;
} }
if (count($this->application->ports_mappings_array) > 0) { if (count($this->application->ports_mappings_array) > 0 || (bool) $this->application->settings->is_consistent_container_name_enabled) {
$this->application_deployment_queue->addLogEntry("----------------------------------------"); $this->application_deployment_queue->addLogEntry("----------------------------------------");
$this->application_deployment_queue->addLogEntry("Application has ports mapped to the host system, rolling update is not supported."); if (count($this->application->ports_mappings_array) > 0) {
$this->application_deployment_queue->addLogEntry("Application has ports mapped to the host system, rolling update is not supported.");
}
if ((bool) $this->application->settings->is_consistent_container_name_enabled) {
$this->application_deployment_queue->addLogEntry("Consistent container name feature enabled, rolling update is not supported.");
}
$this->stop_running_container(force: true); $this->stop_running_container(force: true);
$this->start_by_compose_file(); $this->start_by_compose_file();
} else { } else {
@@ -787,10 +859,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} }
private function deploy_to_additional_destinations() private function deploy_to_additional_destinations()
{ {
if (str($this->application->additional_destinations)->isEmpty()) { if ($this->application->additional_networks->count() === 0) {
return; return;
} }
$destination_ids = collect(str($this->application->additional_destinations)->explode(',')); $destination_ids = $this->application->additional_networks->pluck('id');
if ($this->server->isSwarm()) { if ($this->server->isSwarm()) {
$this->application_deployment_queue->addLogEntry("Additional destinations are not supported in swarm mode."); $this->application_deployment_queue->addLogEntry("Additional destinations are not supported in swarm mode.");
return; return;
@@ -950,11 +1022,15 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->env_nixpacks_args = collect([]); $this->env_nixpacks_args = collect([]);
if ($this->pull_request_id === 0) { if ($this->pull_request_id === 0) {
foreach ($this->application->nixpacks_environment_variables as $env) { foreach ($this->application->nixpacks_environment_variables as $env) {
$this->env_nixpacks_args->push("--env {$env->key}={$env->real_value}"); if (!is_null($env->real_value)) {
$this->env_nixpacks_args->push("--env {$env->key}={$env->real_value}");
}
} }
} else { } else {
foreach ($this->application->nixpacks_environment_variables_preview as $env) { foreach ($this->application->nixpacks_environment_variables_preview as $env) {
$this->env_nixpacks_args->push("--env {$env->key}={$env->real_value}"); if (!is_null($env->real_value)) {
$this->env_nixpacks_args->push("--env {$env->key}={$env->real_value}");
}
} }
} }
@@ -965,11 +1041,15 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->env_args = collect([]); $this->env_args = collect([]);
if ($this->pull_request_id === 0) { if ($this->pull_request_id === 0) {
foreach ($this->application->build_environment_variables as $env) { foreach ($this->application->build_environment_variables as $env) {
$this->env_args->put($env->key, $env->real_value); if (!is_null($env->real_value)) {
$this->env_args->put($env->key, $env->real_value);
}
} }
} else { } else {
foreach ($this->application->build_environment_variables_preview as $env) { foreach ($this->application->build_environment_variables_preview as $env) {
$this->env_args->put($env->key, $env->real_value); if (!is_null($env->real_value)) {
$this->env_args->put($env->key, $env->real_value);
}
} }
} }
$this->env_args->put('SOURCE_COMMIT', $this->commit); $this->env_args->put('SOURCE_COMMIT', $this->commit);
@@ -1134,13 +1214,18 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
// ]; // ];
// } // }
$docker_compose['services'][$this->application->uuid] = $docker_compose['services'][$this->container_name]; if ((bool)$this->application->settings->is_consistent_container_name_enabled) {
$custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options);
data_forget($docker_compose, 'services.' . $this->container_name); if (count($custom_compose) > 0) {
$docker_compose['services'][$this->container_name] = array_merge_recursive($docker_compose['services'][$this->container_name], $custom_compose);
$custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options); }
if (count($custom_compose) > 0) { } else {
$docker_compose['services'][$this->application->uuid] = array_merge_recursive($docker_compose['services'][$this->application->uuid], $custom_compose); $docker_compose['services'][$this->application->uuid] = $docker_compose['services'][$this->container_name];
data_forget($docker_compose, 'services.' . $this->container_name);
$custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options);
if (count($custom_compose) > 0) {
$docker_compose['services'][$this->application->uuid] = array_merge_recursive($docker_compose['services'][$this->application->uuid], $custom_compose);
}
} }
$this->docker_compose = Yaml::dump($docker_compose, 10); $this->docker_compose = Yaml::dump($docker_compose, 10);
@@ -1425,6 +1510,11 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
[executeInDocker($this->deployment_uuid, "docker rm -f $containerName >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true], [executeInDocker($this->deployment_uuid, "docker rm -f $containerName >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true],
); );
}); });
if ($this->application->settings->is_consistent_container_name_enabled) {
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true],
);
}
} else { } else {
$this->application_deployment_queue->addLogEntry("New container is not healthy, rolling back to the old container."); $this->application_deployment_queue->addLogEntry("New container is not healthy, rolling back to the old container.");
$this->application_deployment_queue->update([ $this->application_deployment_queue->update([
@@ -1529,7 +1619,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
return; return;
} }
if ($status === ApplicationDeploymentStatus::FINISHED->value) { if ($status === ApplicationDeploymentStatus::FINISHED->value) {
// $this->deploy_to_additional_destinations(); $this->deploy_to_additional_destinations();
$this->application->environment->project->team?->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview)); $this->application->environment->project->team?->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview));
} }
} }
@@ -1542,10 +1632,14 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
} }
if ($this->application->build_pack !== 'dockercompose') { if ($this->application->build_pack !== 'dockercompose') {
$this->application_deployment_queue->addLogEntry("Deployment failed. Removing the new version of your application.", 'stderr'); $code = $exception->getCode();
$this->execute_remote_command( if ($code !== 69420) {
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true] // 69420 means failed to push the image to the registry, so we don't need to remove the new version as it is the currently running one
); $this->application_deployment_queue->addLogEntry("Deployment failed. Removing the new version of your application.", 'stderr');
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true]
);
}
} }
$this->next(ApplicationDeploymentStatus::FAILED->value); $this->next(ApplicationDeploymentStatus::FAILED->value);

View File

@@ -5,6 +5,7 @@ namespace App\Jobs;
use App\Actions\Database\StartDatabaseProxy; use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Proxy\CheckProxy; use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy; use App\Actions\Proxy\StartProxy;
use App\Actions\Shared\ComplexStatusCheck;
use App\Models\ApplicationPreview; use App\Models\ApplicationPreview;
use App\Models\Server; use App\Models\Server;
use App\Notifications\Container\ContainerRestarted; use App\Notifications\Container\ContainerRestarted;
@@ -42,6 +43,19 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
public function handle() public function handle()
{ {
$applications = $this->server->applications();
foreach ($applications as $application) {
if ($application->additional_servers->count() > 0) {
$is_main_server = $application->destination->server->id === $this->server->id;
if ($is_main_server) {
ComplexStatusCheck::run($application);
$applications = $applications->filter(function ($value, $key) use ($application) {
return $value->id !== $application->id;
});
}
}
}
if (!$this->server->isFunctional()) { if (!$this->server->isFunctional()) {
return 'Server is not ready.'; return 'Server is not ready.';
}; };
@@ -83,7 +97,6 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
}); });
} }
} }
$applications = $this->server->applications();
$databases = $this->server->databases(); $databases = $this->server->databases();
$services = $this->server->services()->get(); $services = $this->server->services()->get();
$previews = $this->server->previews(); $previews = $this->server->previews();
@@ -160,10 +173,9 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
// Notify user that this container should not be there. // Notify user that this container should not be there.
} }
} }
if (data_get($container,'Name') === '/coolify-db') { if (data_get($container, 'Name') === '/coolify-db') {
$foundDatabases[] = 0; $foundDatabases[] = 0;
} }
} }
$serviceLabelId = data_get($labels, 'coolify.serviceId'); $serviceLabelId = data_get($labels, 'coolify.serviceId');
if ($serviceLabelId) { if ($serviceLabelId) {
@@ -209,7 +221,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
} }
$exitedServices = $exitedServices->unique('id'); $exitedServices = $exitedServices->unique('id');
foreach ($exitedServices as $exitedService) { foreach ($exitedServices as $exitedService) {
if ($exitedService->status === 'exited') { if (str($exitedService->status)->startsWith('exited')) {
continue; continue;
} }
$name = data_get($exitedService, 'name'); $name = data_get($exitedService, 'name');
@@ -231,7 +243,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
$notRunningApplications = $applications->pluck('id')->diff($foundApplications); $notRunningApplications = $applications->pluck('id')->diff($foundApplications);
foreach ($notRunningApplications as $applicationId) { foreach ($notRunningApplications as $applicationId) {
$application = $applications->where('id', $applicationId)->first(); $application = $applications->where('id', $applicationId)->first();
if ($application->status === 'exited') { if (str($application->status)->startsWith('exited')) {
continue; continue;
} }
$application->update(['status' => 'exited']); $application->update(['status' => 'exited']);
@@ -256,7 +268,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
$notRunningApplicationPreviews = $previews->pluck('id')->diff($foundApplicationPreviews); $notRunningApplicationPreviews = $previews->pluck('id')->diff($foundApplicationPreviews);
foreach ($notRunningApplicationPreviews as $previewId) { foreach ($notRunningApplicationPreviews as $previewId) {
$preview = $previews->where('id', $previewId)->first(); $preview = $previews->where('id', $previewId)->first();
if ($preview->status === 'exited') { if (str($preview->status)->startsWith('exited')) {
continue; continue;
} }
$preview->update(['status' => 'exited']); $preview->update(['status' => 'exited']);
@@ -281,7 +293,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
$notRunningDatabases = $databases->pluck('id')->diff($foundDatabases); $notRunningDatabases = $databases->pluck('id')->diff($foundDatabases);
foreach ($notRunningDatabases as $database) { foreach ($notRunningDatabases as $database) {
$database = $databases->where('id', $database)->first(); $database = $databases->where('id', $database)->first();
if ($database->status === 'exited') { if (str($database->status)->startsWith('exited')) {
continue; continue;
} }
$database->update(['status' => 'exited']); $database->update(['status' => 'exited']);

View File

@@ -19,6 +19,7 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Artisan;
class DeleteResourceJob implements ShouldQueue, ShouldBeEncrypted class DeleteResourceJob implements ShouldQueue, ShouldBeEncrypted
{ {
@@ -49,8 +50,11 @@ class DeleteResourceJob implements ShouldQueue, ShouldBeEncrypted
break; break;
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
ray($e->getMessage());
send_internal_notification('ContainerStoppingJob failed with: ' . $e->getMessage()); send_internal_notification('ContainerStoppingJob failed with: ' . $e->getMessage());
throw $e; throw $e;
} finally {
Artisan::queue('cleanup:stucked-resources');
} }
} }
} }

View File

@@ -16,7 +16,7 @@ class ServerStatusJob implements ShouldQueue, ShouldBeEncrypted
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public ?int $disk_usage = null; public int|string|null $disk_usage = null;
public $tries = 4; public $tries = 4;
public function backoff(): int public function backoff(): int
{ {
@@ -41,6 +41,15 @@ class ServerStatusJob implements ShouldQueue, ShouldBeEncrypted
throw new \RuntimeException('Server is not ready.'); throw new \RuntimeException('Server is not ready.');
}; };
try { try {
// $this->server->validateConnection();
// $this->server->validateOS();
// $docker_installed = $this->server->validateDockerEngine();
// if (!$docker_installed) {
// $this->server->installDocker();
// $this->server->validateDockerEngine();
// }
// $this->server->validateDockerEngineVersion();
if ($this->server->isFunctional()) { if ($this->server->isFunctional()) {
$this->cleanup(notify: false); $this->cleanup(notify: false);
} }

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Livewire\Admin;
use App\Models\User;
use Illuminate\Support\Facades\Crypt;
use Livewire\Component;
class Index extends Component
{
public $users = [];
public function mount()
{
if (!isCloud()) {
return redirect()->route('dashboard');
}
if (auth()->user()->id !== 0 && session('adminToken') === null) {
return redirect()->route('dashboard');
}
$this->users = User::whereHas('teams', function ($query) {
$query->whereRelation('subscription', 'stripe_subscription_id', '!=', null);
})->get();
}
public function switchUser(int $user_id)
{
$user = User::find($user_id);
auth()->login($user);
if ($user_id === 0) {
session()->forget('adminToken');
} else {
$token_payload = [
'valid' => true,
];
$token = Crypt::encrypt($token_payload);
session(['adminToken' => $token]);
}
return refreshSession();
}
public function render()
{
return view('livewire.admin.index');
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Livewire\Boarding; namespace App\Livewire\Boarding;
use App\Actions\Server\InstallDocker; use App\Actions\Server\InstallDocker;
use App\Enums\ProxyTypes;
use App\Models\PrivateKey; use App\Models\PrivateKey;
use App\Models\Project; use App\Models\Project;
use App\Models\Server; use App\Models\Server;
@@ -121,15 +122,16 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
} }
$this->selectedExistingPrivateKey = $this->createdServer->privateKey->id; $this->selectedExistingPrivateKey = $this->createdServer->privateKey->id;
$this->serverPublicKey = $this->createdServer->privateKey->publicKey(); $this->serverPublicKey = $this->createdServer->privateKey->publicKey();
$this->validateServer(); $this->installServer();
} }
public function getProxyType() public function getProxyType()
{ {
$proxyTypeSet = $this->createdServer->proxy->type; $this->selectProxy(ProxyTypes::TRAEFIK_V2->value);
if (!$proxyTypeSet) { // $proxyTypeSet = $this->createdServer->proxy->type;
$this->currentState = 'select-proxy'; // if (!$proxyTypeSet) {
return; // $this->currentState = 'select-proxy';
} // return;
// }
$this->getProjects(); $this->getProjects();
} }
public function selectExistingPrivateKey() public function selectExistingPrivateKey()
@@ -193,7 +195,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$this->createdServer->settings->is_cloudflare_tunnel = $this->isCloudflareTunnel; $this->createdServer->settings->is_cloudflare_tunnel = $this->isCloudflareTunnel;
$this->createdServer->settings->save(); $this->createdServer->settings->save();
$this->createdServer->addInitialNetwork(); $this->createdServer->addInitialNetwork();
$this->validateServer(); $this->currentState = 'validate-server';
} }
public function installServer() public function installServer()
{ {
@@ -204,7 +206,8 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
try { try {
config()->set('coolify.mux_enabled', false); config()->set('coolify.mux_enabled', false);
instant_remote_process(['uptime'], $this->createdServer, true); // EC2 does not have `uptime` command, lol
instant_remote_process(['ls /'], $this->createdServer, true);
$this->createdServer->settings()->update([ $this->createdServer->settings()->update([
'is_reachable' => true, 'is_reachable' => true,
@@ -219,7 +222,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$dockerVersion = instant_remote_process(["docker version|head -2|grep -i version| awk '{print $2}'"], $this->createdServer, true); $dockerVersion = instant_remote_process(["docker version|head -2|grep -i version| awk '{print $2}'"], $this->createdServer, true);
$dockerVersion = checkMinimumDockerEngineVersion($dockerVersion); $dockerVersion = checkMinimumDockerEngineVersion($dockerVersion);
if (is_null($dockerVersion)) { if (is_null($dockerVersion)) {
$this->currentState = 'install-docker'; $this->currentState = 'validate-server';
throw new \Exception('Docker not found or old version is installed.'); throw new \Exception('Docker not found or old version is installed.');
} }
$this->createdServer->settings()->update([ $this->createdServer->settings()->update([
@@ -227,27 +230,10 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
]); ]);
$this->getProxyType(); $this->getProxyType();
} catch (\Throwable $e) { } catch (\Throwable $e) {
// $this->dockerInstallationStarted = false;
return handleError(error: $e, livewire: $this); return handleError(error: $e, livewire: $this);
} }
} }
public function installDocker() public function selectProxy(?string $proxyType = null)
{
try {
$this->dockerInstallationStarted = true;
$activity = InstallDocker::run($this->createdServer);
$this->dispatch('installDocker');
$this->dispatch('activityMonitor', $activity->id);
} catch (\Throwable $e) {
$this->dockerInstallationStarted = false;
return handleError(error: $e, livewire: $this);
}
}
public function dockerInstalledOrSkipped()
{
$this->validateServer();
}
public function selectProxy(string|null $proxyType = null)
{ {
if (!$proxyType) { if (!$proxyType) {
return $this->getProjects(); return $this->getProjects();

View File

@@ -6,6 +6,7 @@ use App\Models\ApplicationDeploymentQueue;
use App\Models\Project; use App\Models\Project;
use App\Models\Server; use App\Models\Server;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Artisan;
use Livewire\Component; use Livewire\Component;
class Dashboard extends Component class Dashboard extends Component
@@ -19,6 +20,13 @@ class Dashboard extends Component
$this->projects = Project::ownedByCurrentTeam()->get(); $this->projects = Project::ownedByCurrentTeam()->get();
$this->get_deployments(); $this->get_deployments();
} }
public function cleanup_queue()
{
$this->dispatch('success', 'Cleanup started.');
Artisan::queue('app:init', [
'--cleanup-deployments' => 'true'
]);
}
public function get_deployments() public function get_deployments()
{ {
$this->deployments_per_server = ApplicationDeploymentQueue::whereIn("status", ["in_progress", "queued"])->whereIn("server_id", $this->servers->pluck("id"))->get([ $this->deployments_per_server = ApplicationDeploymentQueue::whereIn("status", ["in_progress", "queued"])->whereIn("server_id", $this->servers->pluck("id"))->get([

View File

@@ -8,20 +8,25 @@ use Livewire\Component;
class Advanced extends Component class Advanced extends Component
{ {
public Application $application; public Application $application;
public bool $is_force_https_enabled;
protected $rules = [ protected $rules = [
'application.settings.is_git_submodules_enabled' => 'boolean|required', 'application.settings.is_git_submodules_enabled' => 'boolean|required',
'application.settings.is_git_lfs_enabled' => 'boolean|required', 'application.settings.is_git_lfs_enabled' => 'boolean|required',
'application.settings.is_preview_deployments_enabled' => 'boolean|required', 'application.settings.is_preview_deployments_enabled' => 'boolean|required',
'application.settings.is_auto_deploy_enabled' => 'boolean|required', 'application.settings.is_auto_deploy_enabled' => 'boolean|required',
'application.settings.is_force_https_enabled' => 'boolean|required', 'is_force_https_enabled' => 'boolean|required',
'application.settings.is_log_drain_enabled' => 'boolean|required', 'application.settings.is_log_drain_enabled' => 'boolean|required',
'application.settings.is_gpu_enabled' => 'boolean|required', 'application.settings.is_gpu_enabled' => 'boolean|required',
'application.settings.is_build_server_enabled' => 'boolean|required', 'application.settings.is_build_server_enabled' => 'boolean|required',
'application.settings.is_consistent_container_name_enabled' => 'boolean|required',
'application.settings.gpu_driver' => 'string|required', 'application.settings.gpu_driver' => 'string|required',
'application.settings.gpu_count' => 'string|required', 'application.settings.gpu_count' => 'string|required',
'application.settings.gpu_device_ids' => 'string|required', 'application.settings.gpu_device_ids' => 'string|required',
'application.settings.gpu_options' => 'string|required', 'application.settings.gpu_options' => 'string|required',
]; ];
public function mount() {
$this->is_force_https_enabled = $this->application->settings->is_force_https_enabled;
}
public function instantSave() public function instantSave()
{ {
if ($this->application->isLogDrainEnabled()) { if ($this->application->isLogDrainEnabled()) {
@@ -31,7 +36,8 @@ class Advanced extends Component
return; return;
} }
} }
if ($this->application->settings->is_force_https_enabled) { if ($this->application->settings->is_force_https_enabled !== $this->is_force_https_enabled) {
$this->application->settings->is_force_https_enabled = $this->is_force_https_enabled;
$this->dispatch('resetDefaultLabels', false); $this->dispatch('resetDefaultLabels', false);
} }
$this->application->settings->save(); $this->application->settings->save();

View File

@@ -48,6 +48,8 @@ class DeploymentNavbar extends Component
{ {
try { try {
$kill_command = "docker rm -f {$this->application_deployment_queue->deployment_uuid}"; $kill_command = "docker rm -f {$this->application_deployment_queue->deployment_uuid}";
$server_id = $this->application_deployment_queue->server_id ?? $this->application->destination->server_id;
$server = Server::find($server_id);
if ($this->application_deployment_queue->logs) { if ($this->application_deployment_queue->logs) {
$previous_logs = json_decode($this->application_deployment_queue->logs, associative: true, flags: JSON_THROW_ON_ERROR); $previous_logs = json_decode($this->application_deployment_queue->logs, associative: true, flags: JSON_THROW_ON_ERROR);
@@ -63,8 +65,8 @@ class DeploymentNavbar extends Component
$this->application_deployment_queue->update([ $this->application_deployment_queue->update([
'logs' => json_encode($previous_logs, flags: JSON_THROW_ON_ERROR), 'logs' => json_encode($previous_logs, flags: JSON_THROW_ON_ERROR),
]); ]);
instant_remote_process([$kill_command], $this->server);
} }
instant_remote_process([$kill_command], $server);
} catch (\Throwable $e) { } catch (\Throwable $e) {
ray($e); ray($e);
return handleError($e, $this); return handleError($e, $this);

View File

@@ -126,7 +126,6 @@ class General extends Component
$this->application->save(); $this->application->save();
} }
$this->initialDockerComposeLocation = $this->application->docker_compose_location; $this->initialDockerComposeLocation = $this->application->docker_compose_location;
$this->checkLabelUpdates();
} }
public function instantSave() public function instantSave()
{ {
@@ -164,6 +163,7 @@ class General extends Component
} }
return $domain; return $domain;
} }
public function updatedApplicationBuildPack() public function updatedApplicationBuildPack()
{ {
if ($this->application->build_pack !== 'nixpacks') { if ($this->application->build_pack !== 'nixpacks') {
@@ -184,15 +184,6 @@ class General extends Component
$this->submit(); $this->submit();
$this->dispatch('build_pack_updated'); $this->dispatch('build_pack_updated');
} }
public function checkLabelUpdates()
{
if (md5($this->application->custom_labels) !== md5(implode("|", generateLabelsApplication($this->application)))) {
$this->labelsChanged = true;
} else {
$this->labelsChanged = false;
}
}
public function getWildcardDomain() public function getWildcardDomain()
{ {
$server = data_get($this->application, 'destination.server'); $server = data_get($this->application, 'destination.server');
@@ -212,6 +203,13 @@ class General extends Component
public function updatedApplicationFqdn() public function updatedApplicationFqdn()
{ {
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
return str($domain)->trim()->lower();
});
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
$this->application->save();
$this->resetDefaultLabels(false); $this->resetDefaultLabels(false);
// $this->dispatch('success', 'Labels reset to default!'); // $this->dispatch('success', 'Labels reset to default!');
} }
@@ -238,20 +236,17 @@ class General extends Component
]); ]);
} }
if (data_get($this->application, 'fqdn')) { if (data_get($this->application, 'fqdn')) {
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); $domains = str($this->application->fqdn)->trim()->explode(',');
$domains = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { if ($this->application->additional_servers->count() === 0) {
return str($domain)->trim()->lower(); foreach ($domains as $domain) {
}); if (!validate_dns_entry($domain, $this->application->destination->server)) {
$domains = $domains->unique(); $showToaster && $this->dispatch('error', "Validating DNS ($domain) failed.", "Make sure you have added the DNS records correctly.<br><br>Check this <a target='_blank' class='text-white underline' href='https://coolify.io/docs/dns-settings'>documentation</a> for further help.");
foreach ($domains as $domain) { }
if (!validate_dns_entry($domain, $this->application->destination->server)) {
$showToaster && $this->dispatch('error', "Validating DNS ($domain) failed.","Make sure you have added the DNS records correctly.<br><br>Check this <a target='_blank' class='text-white underline' href='https://coolify.io/docs/dns-settings'>documentation</a> for further help.");
} }
} }
check_fqdn_usage($this->application); check_fqdn_usage($this->application);
$this->application->fqdn = $domains->implode(','); $this->application->fqdn = $domains->implode(',');
} }
if (data_get($this->application, 'custom_docker_run_options')) { if (data_get($this->application, 'custom_docker_run_options')) {
$this->application->custom_docker_run_options = str($this->application->custom_docker_run_options)->trim(); $this->application->custom_docker_run_options = str($this->application->custom_docker_run_options)->trim();
} }
@@ -277,7 +272,6 @@ class General extends Component
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} finally { } finally {
$this->checkLabelUpdates();
$this->isConfigurationChanged = $this->application->isConfigurationChanged(); $this->isConfigurationChanged = $this->application->isConfigurationChanged();
} }
} }

View File

@@ -3,6 +3,8 @@
namespace App\Livewire\Project\Application; namespace App\Livewire\Project\Application;
use App\Actions\Application\StopApplication; use App\Actions\Application\StopApplication;
use App\Events\ApplicationStatusChanged;
use App\Jobs\ComplexContainerStatusJob;
use App\Jobs\ContainerStatusJob; use App\Jobs\ContainerStatusJob;
use App\Jobs\ServerStatusJob; use App\Jobs\ServerStatusJob;
use App\Models\Application; use App\Models\Application;
@@ -31,13 +33,14 @@ class Heading extends Component
{ {
if ($this->application->destination->server->isFunctional()) { if ($this->application->destination->server->isFunctional()) {
dispatch(new ContainerStatusJob($this->application->destination->server)); dispatch(new ContainerStatusJob($this->application->destination->server));
$this->application->refresh(); // $this->application->refresh();
$this->application->previews->each(function ($preview) { // $this->application->previews->each(function ($preview) {
$preview->refresh(); // $preview->refresh();
}); // });
} else { } else {
dispatch(new ServerStatusJob($this->application->destination->server)); dispatch(new ServerStatusJob($this->application->destination->server));
} }
if ($showNotification) $this->dispatch('success', "Application status updated."); if ($showNotification) $this->dispatch('success', "Application status updated.");
} }
@@ -49,15 +52,19 @@ class Heading extends Component
public function deploy(bool $force_rebuild = false) public function deploy(bool $force_rebuild = false)
{ {
if ($this->application->build_pack === 'dockercompose' && is_null($this->application->docker_compose_raw)) { if ($this->application->build_pack === 'dockercompose' && is_null($this->application->docker_compose_raw)) {
$this->dispatch('error', 'Please load a Compose file first.'); $this->dispatch('error', 'Failed to deploy', 'Please load a Compose file first.');
return; return;
} }
if ($this->application->destination->server->isSwarm() && is_null($this->application->docker_registry_image_name)) { if ($this->application->destination->server->isSwarm() && str($this->application->docker_registry_image_name)->isEmpty()) {
$this->dispatch('error', 'To deploy to a Swarm cluster you must set a Docker image name first.'); $this->dispatch('error', 'Failed to deploy.', 'To deploy to a Swarm cluster you must set a Docker image name first.');
return; return;
} }
if (data_get($this->application, 'settings.is_build_server_enabled') && is_null($this->application->docker_registry_image_name)) { if (data_get($this->application, 'settings.is_build_server_enabled') && str($this->application->docker_registry_image_name)->isEmpty()) {
$this->dispatch('error', 'To use a build server you must set a Docker image name first.<br>More information here: <a target="_blank" class="underline" href="https://coolify.io/docs/server/build-server">documentation</a>'); $this->dispatch('error', 'Failed to deploy.', 'To use a build server, you must first set a Docker image.<br>More information here: <a target="_blank" class="underline" href="https://coolify.io/docs/server/build-server">documentation</a>');
return;
}
if ($this->application->additional_servers->count() > 0 && str($this->application->docker_registry_image_name)->isEmpty()) {
$this->dispatch('error', 'Failed to deploy.', 'Before deploying to multiple servers, you must first set a Docker image in the General tab.<br>More information here: <a target="_blank" class="underline" href="https://coolify.io/docs/server/multiple-servers">documentation</a>');
return; return;
} }
$this->setDeploymentUuid(); $this->setDeploymentUuid();
@@ -85,26 +92,20 @@ class Heading extends Component
StopApplication::run($this->application); StopApplication::run($this->application);
$this->application->status = 'exited'; $this->application->status = 'exited';
$this->application->save(); $this->application->save();
$this->application->refresh(); if ($this->application->additional_servers->count() > 0) {
} $this->application->additional_servers->each(function ($server) {
public function restartNew() $server->pivot->status = "exited:unhealthy";
{ $server->pivot->save();
$this->setDeploymentUuid(); });
queue_application_deployment( }
application: $this->application, ApplicationStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id'));
deployment_uuid: $this->deploymentUuid,
restart_only: true,
is_new_deployment: true,
);
return redirect()->route('project.application.deployment.show', [
'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deploymentUuid,
'environment_name' => $this->parameters['environment_name'],
]);
} }
public function restart() public function restart()
{ {
if ($this->application->additional_servers->count() > 0 && str($this->application->docker_registry_image_name)->isEmpty()) {
$this->dispatch('error', 'Failed to deploy', 'Before deploying to multiple servers, you must first set a Docker image in the General tab.<br>More information here: <a target="_blank" class="underline" href="https://coolify.io/docs/server/multiple-servers">documentation</a>');
return;
}
$this->setDeploymentUuid(); $this->setDeploymentUuid();
queue_application_deployment( queue_application_deployment(
application: $this->application, application: $this->application,

View File

@@ -104,7 +104,7 @@ class Index extends Component
'environment_name' => data_get($service, 'environment.name'), 'environment_name' => data_get($service, 'environment.name'),
'service_uuid' => data_get($service, 'uuid') 'service_uuid' => data_get($service, 'uuid')
]); ]);
$service->status = serviceStatus($service); $service->status = $service->status();
} }
return $service; return $service;
}); });

View File

@@ -41,7 +41,7 @@ class FileStorage extends Component
$this->fileStorage->content = null; $this->fileStorage->content = null;
} }
$this->fileStorage->save(); $this->fileStorage->save();
$this->fileStorage->saveStorageOnServer($this->service); $this->fileStorage->saveStorageOnServer();
$this->dispatch('success', 'File updated successfully.'); $this->dispatch('success', 'File updated successfully.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->fileStorage->setRawAttributes($original); $this->fileStorage->setRawAttributes($original);

View File

@@ -27,12 +27,12 @@ class Index extends Component
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
$this->query = request()->query(); $this->query = request()->query();
$this->service = Service::whereUuid($this->parameters['service_uuid'])->firstOrFail(); $this->service = Service::whereUuid($this->parameters['service_uuid'])->firstOrFail();
$service = $this->service->applications()->whereName($this->parameters['service_name'])->first(); $service = $this->service->applications()->whereUuid($this->parameters['stack_service_uuid'])->first();
if ($service) { if ($service) {
$this->serviceApplication = $service; $this->serviceApplication = $service;
$this->serviceApplication->getFilesFromServer(); $this->serviceApplication->getFilesFromServer();
} else { } else {
$this->serviceDatabase = $this->service->databases()->whereName($this->parameters['service_name'])->first(); $this->serviceDatabase = $this->service->databases()->whereUuid($this->parameters['stack_service_uuid'])->first();
$this->serviceDatabase->getFilesFromServer(); $this->serviceDatabase->getFilesFromServer();
} }
$this->s3s = currentTeam()->s3s; $this->s3s = currentTeam()->s3s;

View File

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

View File

@@ -45,8 +45,10 @@ class StackForm extends Component
$this->service->docker_compose_raw = $raw; $this->service->docker_compose_raw = $raw;
$this->submit(); $this->submit();
} }
public function instantSave() { public function instantSave()
{
$this->service->save(); $this->service->save();
$this->dispatch('success', 'Service settings saved successfully.');
} }
public function submit() public function submit()

View File

@@ -2,11 +2,86 @@
namespace App\Livewire\Project\Shared; namespace App\Livewire\Project\Shared;
use App\Actions\Application\StopApplicationOneServer;
use App\Events\ApplicationStatusChanged;
use App\Models\Server;
use App\Models\StandaloneDocker;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2;
class Destination extends Component class Destination extends Component
{ {
public $resource; public $resource;
public $servers = []; public $networks = [];
public $additional_servers = [];
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},ApplicationStatusChanged" => 'loadData',
];
}
public function mount()
{
$this->loadData();
}
public function loadData()
{
$all_networks = collect([]);
$all_networks = $all_networks->push($this->resource->destination);
$all_networks = $all_networks->merge($this->resource->additional_networks);
$this->networks = Server::isUsable()->get()->map(function ($server) {
return $server->standaloneDockers;
})->flatten();
$this->networks = $this->networks->reject(function ($network) use ($all_networks) {
return $all_networks->pluck('id')->contains($network->id);
});
$this->networks = $this->networks->reject(function ($network) {
return $this->resource->destination->server->id == $network->server->id;
});
}
public function redeploy(int $network_id, int $server_id)
{
if ($this->resource->additional_servers->count() > 0 && str($this->resource->docker_registry_image_name)->isEmpty()) {
$this->dispatch('error', 'Failed to deploy.', 'Before deploying to multiple servers, you must first set a Docker image in the General tab.<br>More information here: <a target="_blank" class="underline" href="https://coolify.io/docs/server/multiple-servers">documentation</a>');
return;
}
$deployment_uuid = new Cuid2(7);
$server = Server::find($server_id);
$destination = StandaloneDocker::find($network_id);
queue_application_deployment(
deployment_uuid: $deployment_uuid,
application: $this->resource,
server: $server,
destination: $destination,
no_questions_asked: true,
);
return redirect()->route('project.application.deployment.show', [
'project_uuid' => data_get($this->resource, 'environment.project.uuid'),
'application_uuid' => data_get($this->resource, 'uuid'),
'deployment_uuid' => $deployment_uuid,
'environment_name' => data_get($this->resource, 'environment.name'),
]);
}
public function addServer(int $network_id, int $server_id)
{
$this->resource->additional_networks()->attach($network_id, ['server_id' => $server_id]);
$this->resource->load(['additional_networks']);
ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id'));
$this->loadData();
}
public function removeServer(int $network_id, int $server_id)
{
if ($this->resource->destination->server->id == $server_id && $this->resource->destination->id == $network_id) {
$this->dispatch('error', 'You cannot remove this destination server.', 'You are trying to remove the main server.');
return;
}
$server = Server::find($server_id);
StopApplicationOneServer::run($this->resource, $server);
$this->resource->additional_networks()->detach($network_id, ['server_id' => $server_id]);
$this->resource->load(['additional_networks']);
ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id'));
$this->loadData();
}
} }

View File

@@ -171,7 +171,7 @@ class All extends Component
} }
$environment->save(); $environment->save();
$this->refreshEnvs(); $this->refreshEnvs();
$this->dispatch('success', 'Environment variable added successfully.'); $this->dispatch('success', 'Environment variable added.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -13,8 +13,9 @@ class Form extends Component
public ?string $wildcard_domain = null; public ?string $wildcard_domain = null;
public int $cleanup_after_percentage; public int $cleanup_after_percentage;
public bool $dockerInstallationStarted = false; public bool $dockerInstallationStarted = false;
public bool $revalidate = false;
protected $listeners = ['serverInstalled']; protected $listeners = ['serverInstalled', 'revalidate' => '$refresh'];
protected $rules = [ protected $rules = [
'server.name' => 'required', 'server.name' => 'required',
@@ -28,6 +29,7 @@ class Form extends Component
'server.settings.is_swarm_worker' => 'required|boolean', 'server.settings.is_swarm_worker' => 'required|boolean',
'server.settings.is_build_server' => 'required|boolean', 'server.settings.is_build_server' => 'required|boolean',
'server.settings.concurrent_builds' => 'required|integer|min:1', 'server.settings.concurrent_builds' => 'required|integer|min:1',
'server.settings.dynamic_timeout' => 'required|integer|min:1',
'wildcard_domain' => 'nullable|url', 'wildcard_domain' => 'nullable|url',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
@@ -42,6 +44,8 @@ class Form extends Component
'server.settings.is_swarm_worker' => 'Swarm Worker', 'server.settings.is_swarm_worker' => 'Swarm Worker',
'server.settings.is_build_server' => 'Build Server', 'server.settings.is_build_server' => 'Build Server',
'server.settings.concurrent_builds' => 'Concurrent Builds', 'server.settings.concurrent_builds' => 'Concurrent Builds',
'server.settings.dynamic_timeout' => 'Dynamic Timeout',
]; ];
public function mount() public function mount()
@@ -65,6 +69,10 @@ class Form extends Component
return handleError($e, $this); return handleError($e, $this);
} }
} }
public function revalidate()
{
$this->revalidate = true;
}
public function checkLocalhostConnection() public function checkLocalhostConnection()
{ {
$uptime = $this->server->validateConnection(); $uptime = $this->server->validateConnection();

View File

@@ -12,7 +12,7 @@ class Status extends Component
public Server $server; public Server $server;
public bool $polling = false; public bool $polling = false;
public int $numberOfPolls = 0; public int $numberOfPolls = 0;
protected $listeners = ['proxyStatusUpdated', 'startProxyPolling']; protected $listeners = ['proxyStatusUpdated' => '$refresh', 'startProxyPolling'];
public function startProxyPolling() public function startProxyPolling()
{ {

View File

@@ -0,0 +1,64 @@
<?php
namespace App\Livewire\Server;
use App\Models\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Collection;
use Livewire\Component;
class Resources extends Component
{
use AuthorizesRequests;
public ?Server $server = null;
public $parameters = [];
public Collection $unmanagedContainers;
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},ApplicationStatusChanged" => 'refreshStatus',
];
}
public function startUnmanaged($id) {
$this->server->startUnmanaged($id);
$this->dispatch('success', 'Container started.');
$this->loadUnmanagedContainers();
}
public function restartUnmanaged($id) {
$this->server->restartUnmanaged($id);
$this->dispatch('success', 'Container restarted.');
$this->loadUnmanagedContainers();
}
public function stopUnmanaged($id) {
$this->server->stopUnmanaged($id);
$this->dispatch('success', 'Container stopped.');
$this->loadUnmanagedContainers();
}
public function refreshStatus() {
$this->server->refresh();
$this->loadUnmanagedContainers();
$this->dispatch('success', 'Resource statuses refreshed.');
}
public function loadUnmanagedContainers() {
$this->unmanagedContainers = $this->server->loadUnmanagedContainers();
}
public function mount() {
$this->unmanagedContainers = collect();
$this->parameters = get_route_parameters();
try {
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
if (is_null($this->server)) {
return redirect()->route('server.index');
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
$this->loadUnmanagedContainers();
}
public function render()
{
return view('livewire.server.resources');
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Livewire\Server; namespace App\Livewire\Server;
use App\Actions\Proxy\StartProxy;
use App\Models\Server; use App\Models\Server;
use Livewire\Component; use Livewire\Component;
@@ -14,8 +15,11 @@ class ValidateAndInstall extends Component
public $uptime = null; public $uptime = null;
public $supported_os_type = null; public $supported_os_type = null;
public $docker_installed = null; public $docker_installed = null;
public $docker_compose_installed = null;
public $docker_version = null; public $docker_version = null;
public $proxy_started = false;
public $error = null; public $error = null;
public bool $ask = false;
protected $listeners = ['validateServer' => 'init', 'validateDockerEngine', 'validateServerNow' => 'validateServer']; protected $listeners = ['validateServer' => 'init', 'validateDockerEngine', 'validateServerNow' => 'validateServer'];
@@ -26,11 +30,18 @@ class ValidateAndInstall extends Component
$this->supported_os_type = null; $this->supported_os_type = null;
$this->docker_installed = null; $this->docker_installed = null;
$this->docker_version = null; $this->docker_version = null;
$this->docker_compose_installed = null;
$this->proxy_started = null;
$this->error = null; $this->error = null;
$this->number_of_tries = 0; $this->number_of_tries = 0;
$this->dispatch('validateServerNow'); if (!$this->ask) {
$this->dispatch('validateServerNow');
}
}
public function startValidatingAfterAsking() {
$this->ask = false;
$this->init();
} }
public function validateServer() public function validateServer()
{ {
try { try {
@@ -43,6 +54,11 @@ class ValidateAndInstall extends Component
if ($swarmInstalled) { if ($swarmInstalled) {
$this->dispatch('success', 'Docker Swarm is initiated.'); $this->dispatch('success', 'Docker Swarm is initiated.');
} }
} else {
$proxy = StartProxy::run($this->server);
if ($proxy) {
$this->proxy_started = true;
}
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
@@ -67,9 +83,9 @@ class ValidateAndInstall extends Component
public function validateDockerEngine() public function validateDockerEngine()
{ {
$this->docker_installed = $this->server->validateDockerEngine(); $this->docker_installed = $this->server->validateDockerEngine();
if (!$this->docker_installed) { $this->docker_compose_installed = $this->server->validateDockerCompose();
if (!$this->docker_installed || !$this->docker_compose_installed) {
if ($this->install) { if ($this->install) {
ray($this->number_of_tries, $this->max_tries);
if ($this->number_of_tries == $this->max_tries) { if ($this->number_of_tries == $this->max_tries) {
$this->error = 'Docker Engine could not be installed. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.'; $this->error = 'Docker Engine could not be installed. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
return; return;

View File

@@ -6,6 +6,7 @@ use App\Enums\ApplicationDeploymentStatus;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Spatie\Activitylog\Models\Activity; use Spatie\Activitylog\Models\Activity;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use RuntimeException; use RuntimeException;
@@ -15,7 +16,6 @@ class Application extends BaseModel
{ {
use SoftDeletes; use SoftDeletes;
protected $guarded = []; protected $guarded = [];
protected static function booted() protected static function booted()
{ {
static::saving(function ($application) { static::saving(function ($application) {
@@ -49,10 +49,23 @@ class Application extends BaseModel
$application->persistentStorages()->delete(); $application->persistentStorages()->delete();
$application->environment_variables()->delete(); $application->environment_variables()->delete();
$application->environment_variables_preview()->delete(); $application->environment_variables_preview()->delete();
foreach ($application->scheduled_tasks as $task) {
$task->delete();
}
$application->tags()->detach(); $application->tags()->detach();
}); });
} }
public function additional_servers()
{
return $this->belongsToMany(Server::class, 'additional_destinations')
->withPivot('standalone_docker_id', 'status');
}
public function additional_networks()
{
return $this->belongsToMany(StandaloneDocker::class, 'additional_destinations')
->withPivot('server_id', 'status');
}
public function is_github_based(): bool public function is_github_based(): bool
{ {
if (data_get($this, 'source')) { if (data_get($this, 'source')) {
@@ -191,9 +204,6 @@ class Application extends BaseModel
set: fn ($value) => $value === "" ? null : $value, set: fn ($value) => $value === "" ? null : $value,
); );
} }
// Normal Deployments
public function portsMappingsArray(): Attribute public function portsMappingsArray(): Attribute
{ {
return Attribute::make( return Attribute::make(
@@ -203,6 +213,83 @@ class Application extends BaseModel
); );
} }
public function isExited()
{
return (bool) str($this->status)->startsWith('exited');
}
public function realStatus()
{
return $this->getRawOriginal('status');
}
public function status(): Attribute
{
return Attribute::make(
set: function ($value) {
if ($this->additional_servers->count() === 0) {
if (str($value)->contains('(')) {
$status = str($value)->before('(')->trim()->value();
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
} else if (str($value)->contains(':')) {
$status = str($value)->before(':')->trim()->value();
$health = str($value)->after(':')->trim()->value() ?? 'unhealthy';
} else {
$status = $value;
$health = 'unhealthy';
}
return "$status:$health";
} else {
if (str($value)->contains('(')) {
$status = str($value)->before('(')->trim()->value();
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
} else if (str($value)->contains(':')) {
$status = str($value)->before(':')->trim()->value();
$health = str($value)->after(':')->trim()->value() ?? 'unhealthy';
} else {
$status = $value;
$health = 'unhealthy';
}
return "$status:$health";
}
},
get: function ($value) {
if ($this->additional_servers->count() === 0) {
//running (healthy)
if (str($value)->contains('(')) {
$status = str($value)->before('(')->trim()->value();
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
} else if (str($value)->contains(':')) {
$status = str($value)->before(':')->trim()->value();
$health = str($value)->after(':')->trim()->value() ?? 'unhealthy';
} else {
$status = $value;
$health = 'unhealthy';
}
return "$status:$health";
} else {
$complex_status = null;
$complex_health = null;
$complex_status = $main_server_status = str($value)->before(':')->value();
$complex_health = $main_server_health = str($value)->after(':')->value() ?? 'unhealthy';
$additional_servers_status = $this->additional_servers->pluck('pivot.status');
foreach ($additional_servers_status as $status) {
$server_status = str($status)->before(':')->value();
$server_health = str($status)->after(':')->value() ?? 'unhealthy';
if ($server_status !== 'running') {
if ($main_server_status !== $server_status) {
$complex_status = 'degraded';
}
}
if ($server_health !== 'healthy') {
if ($main_server_health !== $server_health) {
$complex_health = 'unhealthy';
}
}
}
return "$complex_status:$complex_health";
}
},
);
}
public function portsExposesArray(): Attribute public function portsExposesArray(): Attribute
{ {
@@ -216,7 +303,8 @@ class Application extends BaseModel
{ {
return $this->morphToMany(Tag::class, 'taggable'); return $this->morphToMany(Tag::class, 'taggable');
} }
public function project() { public function project()
{
return data_get($this, 'environment.project'); return data_get($this, 'environment.project');
} }
public function team() public function team()
@@ -387,7 +475,7 @@ class Application extends BaseModel
{ {
return data_get($this, 'settings.is_log_drain_enabled', false); return data_get($this, 'settings.is_log_drain_enabled', false);
} }
public function isConfigurationChanged($save = false) public function isConfigurationChanged(bool $save = false)
{ {
$newConfigHash = $this->fqdn . $this->git_repository . $this->git_branch . $this->git_commit_sha . $this->build_pack . $this->static_image . $this->install_command . $this->build_command . $this->start_command . $this->port_exposes . $this->port_mappings . $this->base_directory . $this->publish_directory . $this->dockerfile . $this->dockerfile_location . $this->custom_labels; $newConfigHash = $this->fqdn . $this->git_repository . $this->git_branch . $this->git_commit_sha . $this->build_pack . $this->static_image . $this->install_command . $this->build_command . $this->start_command . $this->port_exposes . $this->port_mappings . $this->base_directory . $this->publish_directory . $this->dockerfile . $this->dockerfile_location . $this->custom_labels;
if ($this->pull_request_id === 0 || $this->pull_request_id === null) { if ($this->pull_request_id === 0 || $this->pull_request_id === null) {
@@ -435,17 +523,21 @@ class Application extends BaseModel
{ {
return "/artifacts/{$uuid}"; return "/artifacts/{$uuid}";
} }
function setGitImportSettings(string $deployment_uuid, string $git_clone_command) function setGitImportSettings(string $deployment_uuid, string $git_clone_command, bool $public = false)
{ {
$baseDir = $this->generateBaseDir($deployment_uuid); $baseDir = $this->generateBaseDir($deployment_uuid);
if ($this->git_commit_sha !== 'HEAD') { if ($this->git_commit_sha !== 'HEAD') {
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && git -c advice.detachedHead=false checkout {$this->git_commit_sha} >/dev/null 2>&1"; $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git -c advice.detachedHead=false checkout {$this->git_commit_sha} >/dev/null 2>&1";
} }
if ($this->settings->is_git_submodules_enabled) { if ($this->settings->is_git_submodules_enabled) {
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && git submodule update --init --recursive"; if ($public) {
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && sed -i \"s#git@\(.*\):#https://\\1/#g\" {$baseDir}/.gitmodules || true";
}
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git submodule update --init --recursive";
} }
if ($this->settings->is_git_lfs_enabled) { if ($this->settings->is_git_lfs_enabled) {
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && git lfs pull"; $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git lfs pull";
} }
return $git_clone_command; return $git_clone_command;
} }
@@ -473,7 +565,7 @@ class Application extends BaseModel
$fullRepoUrl = "{$this->source->html_url}/{$customRepository}"; $fullRepoUrl = "{$this->source->html_url}/{$customRepository}";
$git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$customRepository} {$baseDir}"; $git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$customRepository} {$baseDir}";
if (!$only_checkout) { if (!$only_checkout) {
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command); $git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command, public: true);
} }
if ($exec_in_docker) { if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, $git_clone_command)); $commands->push(executeInDocker($deployment_uuid, $git_clone_command));
@@ -572,7 +664,7 @@ class Application extends BaseModel
if ($this->deploymentType() === 'other') { if ($this->deploymentType() === 'other') {
$fullRepoUrl = $customRepository; $fullRepoUrl = $customRepository;
$git_clone_command = "{$git_clone_command} {$customRepository} {$baseDir}"; $git_clone_command = "{$git_clone_command} {$customRepository} {$baseDir}";
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command); $git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command, public: true);
if ($pull_request_id !== 0) { if ($pull_request_id !== 0) {
if ($git_type === 'gitlab') { if ($git_type === 'gitlab') {

View File

@@ -26,7 +26,6 @@ class Environment extends Model
{ {
return $this->hasMany(Application::class); return $this->hasMany(Application::class);
} }
public function postgresqls() public function postgresqls()
{ {
return $this->hasMany(StandalonePostgresql::class); return $this->hasMany(StandalonePostgresql::class);

View File

@@ -10,20 +10,37 @@ class LocalFileVolume extends BaseModel
use HasFactory; use HasFactory;
protected $guarded = []; protected $guarded = [];
protected static function booted()
{
static::created(function (LocalFileVolume $fileVolume) {
$fileVolume->load(['service']);
$fileVolume->saveStorageOnServer();
});
}
public function service() public function service()
{ {
return $this->morphTo('resource'); return $this->morphTo('resource');
} }
public function saveStorageOnServer(ServiceApplication|ServiceDatabase $service) public function saveStorageOnServer()
{ {
$workdir = $service->service->workdir(); $workdir = $this->resource->service->workdir();
$server = $service->service->server; $server = $this->resource->service->server;
$commands = collect([ $commands = collect([
"mkdir -p $workdir > /dev/null 2>&1 || true", "mkdir -p $workdir > /dev/null 2>&1 || true",
"cd $workdir" "cd $workdir"
]); ]);
$is_directory = $this->is_directory;
if ($is_directory) {
$commands->push("mkdir -p $this->fs_path > /dev/null 2>&1 || true");
}
if (str($this->fs_path)->startsWith('.') || str($this->fs_path)->startsWith('/') || str($this->fs_path)->startsWith('~')) {
$parent_dir = str($this->fs_path)->beforeLast('/');
if ($parent_dir != '') {
$commands->push("mkdir -p $parent_dir > /dev/null 2>&1 || true");
}
}
$fileVolume = $this; $fileVolume = $this;
$path = Str::of(data_get($fileVolume, 'fs_path')); $path = str(data_get($fileVolume, 'fs_path'));
$content = data_get($fileVolume, 'content'); $content = data_get($fileVolume, 'content');
if ($path->startsWith('.')) { if ($path->startsWith('.')) {
$path = $path->after('.'); $path = $path->after('.');
@@ -32,17 +49,18 @@ class LocalFileVolume extends BaseModel
$isFile = instant_remote_process(["test -f $path && echo OK || echo NOK"], $server); $isFile = instant_remote_process(["test -f $path && echo OK || echo NOK"], $server);
$isDir = instant_remote_process(["test -d $path && echo OK || echo NOK"], $server); $isDir = instant_remote_process(["test -d $path && echo OK || echo NOK"], $server);
if ($isFile == 'OK' && $fileVolume->is_directory) { if ($isFile == 'OK' && $fileVolume->is_directory) {
throw new \Exception("File $path is a file on the server, but you are trying to mark it as a directory. Please delete the file on the server or mark it as directory."); throw new \Exception("The following file is a file on the server, but you are trying to mark it as a directory. Please delete the file on the server or mark it as directory.");
} else if ($isDir == 'OK' && !$fileVolume->is_directory) { } else if ($isDir == 'OK' && !$fileVolume->is_directory) {
throw new \Exception("File $path is a directory on the server, but you are trying to mark it as a file. Please delete the directory on the server or mark it as directory."); throw new \Exception("The following file is a directory on the server, but you are trying to mark it as a file. <br><br>Please delete the directory on the server or mark it as directory.");
} }
if (!$fileVolume->is_directory && $isDir == 'NOK') { if (!$fileVolume->is_directory && $isDir == 'NOK') {
$content = base64_encode($content); if ($content) {
$commands->push("echo '$content' | base64 -d > $path"); $content = base64_encode($content);
} else if ($isDir == 'NOK' && $fileVolume->is_directory) { $commands->push("echo '$content' | base64 -d > $path");
}
} else if ($isDir == 'NOK' && $fileVolume->is_directory) {
$commands->push("mkdir -p $path > /dev/null 2>&1 || true"); $commands->push("mkdir -p $path > /dev/null 2>&1 || true");
} }
ray($commands->toArray());
return instant_remote_process($commands, $server); return instant_remote_process($commands, $server);
} }
} }

View File

@@ -6,7 +6,10 @@ use Illuminate\Database\Eloquent\Model;
class ProjectSetting extends Model class ProjectSetting extends Model
{ {
protected $fillable = [ protected $guarded = [];
'project_id'
]; public function project()
{
return $this->belongsTo(Project::class);
}
} }

View File

@@ -9,6 +9,7 @@ use App\Notifications\Server\Revived;
use App\Notifications\Server\Unreachable; use App\Notifications\Server\Unreachable;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Support\Facades\DB;
use Spatie\SchemalessAttributes\Casts\SchemalessAttributes; use Spatie\SchemalessAttributes\Casts\SchemalessAttributes;
use Spatie\SchemalessAttributes\SchemalessAttributesTrait; use Spatie\SchemalessAttributes\SchemalessAttributesTrait;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@@ -224,6 +225,32 @@ class Server extends BaseModel
$services = $this->services(); $services = $this->services();
return $applications->concat($databases)->concat($services->get()); return $applications->concat($databases)->concat($services->get());
} }
public function stopUnmanaged($id)
{
return instant_remote_process(["docker stop -t 0 $id"], $this);
}
public function restartUnmanaged($id)
{
return instant_remote_process(["docker restart $id"], $this);
}
public function startUnmanaged($id)
{
return instant_remote_process(["docker start $id"], $this);
}
public function loadUnmanagedContainers()
{
$containers = instant_remote_process(["docker ps -a --format '{{json .}}' "], $this);
$containers = format_docker_command_output_to_json($containers);
$containers = $containers->map(function ($container) {
$labels = data_get($container, 'Labels');
if (!str($labels)->contains("coolify.managed")) {
return $container;
}
return null;
});
$containers = $containers->filter();
return collect($containers);
}
public function hasDefinedResources() public function hasDefinedResources()
{ {
$applications = $this->applications()->count() > 0; $applications = $this->applications()->count() > 0;
@@ -244,13 +271,23 @@ class Server extends BaseModel
$mysqls = data_get($standaloneDocker, 'mysqls', collect([])); $mysqls = data_get($standaloneDocker, 'mysqls', collect([]));
$mariadbs = data_get($standaloneDocker, 'mariadbs', collect([])); $mariadbs = data_get($standaloneDocker, 'mariadbs', collect([]));
return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs); return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs);
})->filter(function ($item) {
return data_get($item, 'name') !== 'coolify-db';
})->flatten(); })->flatten();
} }
public function applications() public function applications()
{ {
return $this->destinations()->map(function ($standaloneDocker) { $applications = $this->destinations()->map(function ($standaloneDocker) {
return $standaloneDocker->applications; return $standaloneDocker->applications;
})->flatten(); })->flatten();
$additionalApplicationIds = DB::table('additional_destinations')->where('server_id', $this->id)->get('application_id');
$additionalApplicationIds = collect($additionalApplicationIds)->map(function ($item) {
return $item->application_id;
});
Application::whereIn('id', $additionalApplicationIds)->get()->each(function ($application) use ($applications) {
$applications->push($application);
});
return $applications;
} }
public function dockerComposeBasedApplications() public function dockerComposeBasedApplications()
{ {
@@ -300,7 +337,8 @@ class Server extends BaseModel
{ {
$standalone_docker = $this->hasMany(StandaloneDocker::class)->get(); $standalone_docker = $this->hasMany(StandaloneDocker::class)->get();
$swarm_docker = $this->hasMany(SwarmDocker::class)->get(); $swarm_docker = $this->hasMany(SwarmDocker::class)->get();
return $standalone_docker->concat($swarm_docker); $asd = $this->belongsToMany(StandaloneDocker::class, 'additional_destinations')->withPivot('server_id')->get();
return $standalone_docker->concat($swarm_docker)->concat($asd);
} }
public function standaloneDockers() public function standaloneDockers()
@@ -390,7 +428,9 @@ class Server extends BaseModel
if ($server->skipServer()) { if ($server->skipServer()) {
return false; return false;
} }
$uptime = instant_remote_process(['uptime'], $server, false); // EC2 does not have `uptime` command, lol
$uptime = instant_remote_process(['ls /'], $server, false);
if (!$uptime) { if (!$uptime) {
$server->settings()->update([ $server->settings()->update([
'is_reachable' => false, 'is_reachable' => false,
@@ -433,6 +473,21 @@ class Server extends BaseModel
$this->validateCoolifyNetwork(isSwarm: false, isBuildServer: $this->settings->is_build_server); $this->validateCoolifyNetwork(isSwarm: false, isBuildServer: $this->settings->is_build_server);
return true; return true;
} }
public function validateDockerCompose($throwError = false)
{
$dockerCompose = instant_remote_process(["docker compose version"], $this, false);
if (is_null($dockerCompose)) {
$this->settings->is_usable = false;
$this->settings->save();
if ($throwError) {
throw new \Exception('Server is not usable. Docker Compose is not installed.');
}
return false;
}
$this->settings->is_usable = true;
$this->settings->save();
return true;
}
public function validateDockerSwarm() public function validateDockerSwarm()
{ {
$swarmStatus = instant_remote_process(["docker info|grep -i swarm"], $this, false); $swarmStatus = instant_remote_process(["docker info|grep -i swarm"], $this, false);

View File

@@ -28,6 +28,48 @@ class Service extends BaseModel
{ {
return $this->morphToMany(Tag::class, 'taggable'); return $this->morphToMany(Tag::class, 'taggable');
} }
public function status() {
$foundRunning = false;
$isDegraded = false;
$foundRestaring = false;
$applications = $this->applications;
$databases = $this->databases;
foreach ($applications as $application) {
if ($application->exclude_from_status) {
continue;
}
if (Str::of($application->status)->startsWith('running')) {
$foundRunning = true;
} else if (Str::of($application->status)->startsWith('restarting')) {
$foundRestaring = true;
} else {
$isDegraded = true;
}
}
foreach ($databases as $database) {
if ($database->exclude_from_status) {
continue;
}
if (Str::of($database->status)->startsWith('running')) {
$foundRunning = true;
} else if (Str::of($database->status)->startsWith('restarting')) {
$foundRestaring = true;
} else {
$isDegraded = true;
}
}
if ($foundRestaring) {
return 'degraded';
}
if ($foundRunning && !$isDegraded) {
return 'running';
} else if ($foundRunning && $isDegraded) {
return 'degraded';
} else if (!$foundRunning && !$isDegraded) {
return 'exited';
}
return 'exited';
}
public function extraFields() public function extraFields()
{ {
$fields = collect([]); $fields = collect([]);

View File

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

View File

@@ -21,6 +21,10 @@ class ServiceDatabase extends BaseModel
{ {
return data_get($this, 'is_log_drain_enabled', false); return data_get($this, 'is_log_drain_enabled', false);
} }
public function isGzipEnabled()
{
return true;
}
public function type() public function type()
{ {
return 'service'; return 'service';

View File

@@ -43,11 +43,48 @@ class StandaloneMariadb extends BaseModel
$database->tags()->detach(); $database->tags()->detach();
}); });
} }
public function realStatus()
{
return $this->getRawOriginal('status');
}
public function status(): Attribute
{
return Attribute::make(
set: function ($value) {
if (str($value)->contains('(')) {
$status = str($value)->before('(')->trim()->value();
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
} else if (str($value)->contains(':')) {
$status = str($value)->before(':')->trim()->value();
$health = str($value)->after(':')->trim()->value() ?? 'unhealthy';
} else {
$status = $value;
$health = 'unhealthy';
}
return "$status:$health";
},
get: function ($value) {
if (str($value)->contains('(')) {
$status = str($value)->before('(')->trim()->value();
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
} else if (str($value)->contains(':')) {
$status = str($value)->before(':')->trim()->value();
$health = str($value)->after(':')->trim()->value() ?? 'unhealthy';
} else {
$status = $value;
$health = 'unhealthy';
}
return "$status:$health";
},
);
}
public function tags() public function tags()
{ {
return $this->morphToMany(Tag::class, 'taggable'); return $this->morphToMany(Tag::class, 'taggable');
} }
public function project() {
return data_get($this, 'environment.project');
}
public function team() public function team()
{ {
return data_get($this, 'environment.project.team'); return data_get($this, 'environment.project.team');

View File

@@ -46,11 +46,48 @@ class StandaloneMongodb extends BaseModel
$database->tags()->detach(); $database->tags()->detach();
}); });
} }
public function realStatus()
{
return $this->getRawOriginal('status');
}
public function status(): Attribute
{
return Attribute::make(
set: function ($value) {
if (str($value)->contains('(')) {
$status = str($value)->before('(')->trim()->value();
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
} else if (str($value)->contains(':')) {
$status = str($value)->before(':')->trim()->value();
$health = str($value)->after(':')->trim()->value() ?? 'unhealthy';
} else {
$status = $value;
$health = 'unhealthy';
}
return "$status:$health";
},
get: function ($value) {
if (str($value)->contains('(')) {
$status = str($value)->before('(')->trim()->value();
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
} else if (str($value)->contains(':')) {
$status = str($value)->before(':')->trim()->value();
$health = str($value)->after(':')->trim()->value() ?? 'unhealthy';
} else {
$status = $value;
$health = 'unhealthy';
}
return "$status:$health";
},
);
}
public function tags() public function tags()
{ {
return $this->morphToMany(Tag::class, 'taggable'); return $this->morphToMany(Tag::class, 'taggable');
} }
public function project() {
return data_get($this, 'environment.project');
}
public function team() public function team()
{ {
return data_get($this, 'environment.project.team'); return data_get($this, 'environment.project.team');

View File

@@ -43,11 +43,48 @@ class StandaloneMysql extends BaseModel
$database->tags()->detach(); $database->tags()->detach();
}); });
} }
public function realStatus()
{
return $this->getRawOriginal('status');
}
public function status(): Attribute
{
return Attribute::make(
set: function ($value) {
if (str($value)->contains('(')) {
$status = str($value)->before('(')->trim()->value();
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
} else if (str($value)->contains(':')) {
$status = str($value)->before(':')->trim()->value();
$health = str($value)->after(':')->trim()->value() ?? 'unhealthy';
} else {
$status = $value;
$health = 'unhealthy';
}
return "$status:$health";
},
get: function ($value) {
if (str($value)->contains('(')) {
$status = str($value)->before('(')->trim()->value();
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
} else if (str($value)->contains(':')) {
$status = str($value)->before(':')->trim()->value();
$health = str($value)->after(':')->trim()->value() ?? 'unhealthy';
} else {
$status = $value;
$health = 'unhealthy';
}
return "$status:$health";
},
);
}
public function tags() public function tags()
{ {
return $this->morphToMany(Tag::class, 'taggable'); return $this->morphToMany(Tag::class, 'taggable');
} }
public function project() {
return data_get($this, 'environment.project');
}
public function team() public function team()
{ {
return data_get($this, 'environment.project.team'); return data_get($this, 'environment.project.team');

View File

@@ -43,11 +43,49 @@ class StandalonePostgresql extends BaseModel
$database->tags()->detach(); $database->tags()->detach();
}); });
} }
public function realStatus()
{
return $this->getRawOriginal('status');
}
public function status(): Attribute
{
return Attribute::make(
set: function ($value) {
if (str($value)->contains('(')) {
$status = str($value)->before('(')->trim()->value();
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
} else if (str($value)->contains(':')) {
$status = str($value)->before(':')->trim()->value();
$health = str($value)->after(':')->trim()->value() ?? 'unhealthy';
} else {
$status = $value;
$health = 'unhealthy';
}
return "$status:$health";
},
get: function ($value) {
if (str($value)->contains('(')) {
$status = str($value)->before('(')->trim()->value();
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
} else if (str($value)->contains(':')) {
$status = str($value)->before(':')->trim()->value();
$health = str($value)->after(':')->trim()->value() ?? 'unhealthy';
} else {
$status = $value;
$health = 'unhealthy';
}
return "$status:$health";
},
);
}
public function tags() public function tags()
{ {
return $this->morphToMany(Tag::class, 'taggable'); return $this->morphToMany(Tag::class, 'taggable');
} }
public function project()
{
return data_get($this, 'environment.project');
}
public function link() public function link()
{ {
if (data_get($this, 'environment.project.uuid')) { if (data_get($this, 'environment.project.uuid')) {

View File

@@ -38,11 +38,49 @@ class StandaloneRedis extends BaseModel
$database->tags()->detach(); $database->tags()->detach();
}); });
} }
public function realStatus()
{
return $this->getRawOriginal('status');
}
public function status(): Attribute
{
return Attribute::make(
set: function ($value) {
if (str($value)->contains('(')) {
$status = str($value)->before('(')->trim()->value();
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
} else if (str($value)->contains(':')) {
$status = str($value)->before(':')->trim()->value();
$health = str($value)->after(':')->trim()->value() ?? 'unhealthy';
} else {
$status = $value;
$health = 'unhealthy';
}
return "$status:$health";
},
get: function ($value) {
if (str($value)->contains('(')) {
$status = str($value)->before('(')->trim()->value();
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
} else if (str($value)->contains(':')) {
$status = str($value)->before(':')->trim()->value();
$health = str($value)->after(':')->trim()->value() ?? 'unhealthy';
} else {
$status = $value;
$health = 'unhealthy';
}
return "$status:$health";
},
);
}
public function tags() public function tags()
{ {
return $this->morphToMany(Tag::class, 'taggable'); return $this->morphToMany(Tag::class, 'taggable');
} }
public function project()
{
return data_get($this, 'environment.project');
}
public function team() public function team()
{ {
return data_get($this, 'environment.project.team'); return data_get($this, 'environment.project.team');

View File

@@ -15,8 +15,9 @@ class Services extends Component
public function __construct( public function __construct(
public Service $service, public Service $service,
public string $complexStatus = 'exited', public string $complexStatus = 'exited',
public bool $showRefreshButton = true
) { ) {
$this->complexStatus = serviceStatus($service); $this->complexStatus = $service->status();
} }
/** /**

View File

@@ -0,0 +1,7 @@
<?php
function get_team_id_from_token()
{
$token = auth()->user()->currentAccessToken();
return data_get($token, 'team_id');
}

View File

@@ -13,6 +13,11 @@ const VALID_CRON_STRINGS = [
const RESTART_MODE = 'unless-stopped'; const RESTART_MODE = 'unless-stopped';
const DATABASE_DOCKER_IMAGES = [ const DATABASE_DOCKER_IMAGES = [
'bitnami/mariadb',
'bitnami/mongodb',
'bitnami/mysql',
'bitnami/postgresql',
'bitnami/redis',
'mysql', 'mysql',
'mariadb', 'mariadb',
'postgres', 'postgres',

View File

@@ -7,7 +7,6 @@ use App\Models\ServiceApplication;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Spatie\Url\Url; use Spatie\Url\Url;
use Visus\Cuid2\Cuid2;
function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pullRequestId = null): Collection function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pullRequestId = null): Collection
{ {
@@ -123,10 +122,14 @@ function getContainerStatus(Server $server, string $container_id, bool $all_data
function generateApplicationContainerName(Application $application, $pull_request_id = 0) function generateApplicationContainerName(Application $application, $pull_request_id = 0)
{ {
$consistent_container_name = $application->settings->is_consistent_container_name_enabled;
$now = now()->format('Hisu'); $now = now()->format('Hisu');
if ($pull_request_id !== 0 && $pull_request_id !== null) { if ($pull_request_id !== 0 && $pull_request_id !== null) {
return $application->uuid . '-pr-' . $pull_request_id; return $application->uuid . '-pr-' . $pull_request_id;
} else { } else {
if ($consistent_container_name) {
return $application->uuid;
}
return $application->uuid . '-' . $now; return $application->uuid . '-' . $now;
} }
} }
@@ -209,15 +212,48 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource,
} }
return $payload; return $payload;
} }
function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null) function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true)
{ {
$labels = collect([]); $labels = collect([]);
$labels->push('traefik.enable=true'); $labels->push('traefik.enable=true');
$labels->push("traefik.http.middlewares.gzip.compress=true"); $labels->push("traefik.http.middlewares.gzip.compress=true");
$labels->push("traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"); $labels->push("traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https");
$basic_auth = false;
$basic_auth_middleware = null;
$redirect = false;
$redirect_middleware = null;
if ($serviceLabels) {
$basic_auth = $serviceLabels->contains(function ($value) {
return str_contains($value, 'basicauth');
});
if ($basic_auth) {
$basic_auth_middleware = $serviceLabels
->map(function ($item) {
if (preg_match('/traefik\.http\.middlewares\.(.*?)\.basicauth\.users/', $item, $matches)) {
return $matches[1];
}
})
->filter()
->first();
}
$redirect = $serviceLabels->contains(function ($value) {
return str_contains($value, 'redirectregex');
});
if ($redirect) {
$redirect_middleware = $serviceLabels
->map(function ($item) {
if (preg_match('/traefik\.http\.middlewares\.(.*?)\.redirectregex\.regex/', $item, $matches)) {
return $matches[1];
}
})
->filter()
->first();
}
}
foreach ($domains as $loop => $domain) { foreach ($domains as $loop => $domain) {
try { try {
$uuid = new Cuid2(7); // $uuid = new Cuid2(7);
$url = Url::fromString($domain); $url = Url::fromString($domain);
$host = $url->getHost(); $host = $url->getHost();
$path = $url->getPath(); $path = $url->getPath();
@@ -239,11 +275,36 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
} }
if ($path !== '/') { if ($path !== '/') {
$labels->push("traefik.http.middlewares.{$https_label}-stripprefix.stripprefix.prefixes={$path}"); $labels->push("traefik.http.middlewares.{$https_label}-stripprefix.stripprefix.prefixes={$path}");
$labels->push("traefik.http.routers.{$https_label}.middlewares={$https_label}-stripprefix,gzip"); $middlewares = collect(["{$https_label}-stripprefix"]);
if ($is_gzip_enabled) {
$middlewares->push('gzip');
}
if ($basic_auth && $basic_auth_middleware) {
$middlewares->push($basic_auth_middleware);
}
if ($redirect && $redirect_middleware) {
$middlewares->push($redirect_middleware);
}
if ($middlewares->isNotEmpty()) {
$middlewares = $middlewares->join(',');
$labels->push("traefik.http.routers.{$https_label}.middlewares={$middlewares}");
}
} else { } else {
$labels->push("traefik.http.routers.{$https_label}.middlewares=gzip"); $middlewares = collect([]);
if ($is_gzip_enabled) {
$middlewares->push('gzip');
}
if ($basic_auth && $basic_auth_middleware) {
$middlewares->push($basic_auth_middleware);
}
if ($redirect && $redirect_middleware) {
$middlewares->push($redirect_middleware);
}
if ($middlewares->isNotEmpty()) {
$middlewares = $middlewares->join(',');
$labels->push("traefik.http.routers.{$https_label}.middlewares={$middlewares}");
}
} }
$labels->push("traefik.http.routers.{$https_label}.tls=true"); $labels->push("traefik.http.routers.{$https_label}.tls=true");
$labels->push("traefik.http.routers.{$https_label}.tls.certresolver=letsencrypt"); $labels->push("traefik.http.routers.{$https_label}.tls.certresolver=letsencrypt");
@@ -267,16 +328,41 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
} }
if ($path !== '/') { if ($path !== '/') {
$labels->push("traefik.http.middlewares.{$http_label}-stripprefix.stripprefix.prefixes={$path}"); $labels->push("traefik.http.middlewares.{$http_label}-stripprefix.stripprefix.prefixes={$path}");
$labels->push("traefik.http.routers.{$http_label}.middlewares={$http_label}-stripprefix,gzip"); $middlewares = collect(["{$http_label}-stripprefix"]);
if ($is_gzip_enabled) {
$middlewares->push('gzip');
}
if ($basic_auth && $basic_auth_middleware) {
$middlewares->push($basic_auth_middleware);
}
if ($redirect && $redirect_middleware) {
$middlewares->push($redirect_middleware);
}
if ($middlewares->isNotEmpty()) {
$middlewares = $middlewares->join(',');
$labels->push("traefik.http.routers.{$http_label}.middlewares={$middlewares}");
}
} else { } else {
$labels->push("traefik.http.routers.{$http_label}.middlewares=gzip"); $middlewares = collect([]);
if ($is_gzip_enabled) {
$middlewares->push('gzip');
}
if ($basic_auth && $basic_auth_middleware) {
$middlewares->push($basic_auth_middleware);
}
if ($redirect && $redirect_middleware) {
$middlewares->push($redirect_middleware);
}
if ($middlewares->isNotEmpty()) {
$middlewares = $middlewares->join(',');
$labels->push("traefik.http.routers.{$http_label}.middlewares={$middlewares}");
}
} }
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
continue; continue;
} }
} }
return $labels->sort(); return $labels->sort();
} }
function generateLabelsApplication(Application $application, ?ApplicationPreview $preview = null): array function generateLabelsApplication(Application $application, ?ApplicationPreview $preview = null): array
@@ -340,25 +426,20 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null
'--cap-drop' => 'cap_drop', '--cap-drop' => 'cap_drop',
'--security-opt' => 'security_opt', '--security-opt' => 'security_opt',
'--sysctl' => 'sysctls', '--sysctl' => 'sysctls',
'--device' => 'devices',
'--ulimit' => 'ulimits', '--ulimit' => 'ulimits',
'--device' => 'devices',
'--init' => 'init', '--init' => 'init',
'--ulimit' => 'ulimits', '--ulimit' => 'ulimits',
'--privileged' => 'privileged', '--privileged' => 'privileged',
]); ]);
foreach ($matches as $match) { foreach ($matches as $match) {
$option = $match[1]; $option = $match[1];
$value = isset($match[2]) && $match[2] !== '' ? $match[2] : true; if (isset($match[2]) && $match[2] !== '') {
if ($list_options->contains($option)) { $value = $match[2];
$value = explode(',', $value); $options[$option][] = $value;
} $options[$option] = array_unique($options[$option]);
if (array_key_exists($option, $options)) {
if (is_array($options[$option])) {
$options[$option][] = $value;
} else {
$options[$option] = [$options[$option], $value];
}
} else { } else {
$value = true;
$options[$option] = $value; $options[$option] = $value;
} }
} }
@@ -370,7 +451,7 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null
} }
if ($option === '--ulimit') { if ($option === '--ulimit') {
$ulimits = collect([]); $ulimits = collect([]);
collect($value)->map(function ($ulimit) use ($ulimits){ collect($value)->map(function ($ulimit) use ($ulimits) {
$ulimit = explode('=', $ulimit); $ulimit = explode('=', $ulimit);
$type = $ulimit[0]; $type = $ulimit[0];
$limits = explode(':', $ulimit[1]); $limits = explode(':', $ulimit[1]);
@@ -381,7 +462,6 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null
'soft' => $soft_limit, 'soft' => $soft_limit,
'hard' => $hard_limit 'hard' => $hard_limit
]); ]);
} else { } else {
$soft_limit = $ulimit[1]; $soft_limit = $ulimit[1];
$ulimits->put($type, [ $ulimits->put($type, [

View File

@@ -103,6 +103,7 @@ function generate_default_proxy_configuration(Server $server)
"traefik.http.routers.traefik.entrypoints=http", "traefik.http.routers.traefik.entrypoints=http",
"traefik.http.routers.traefik.service=api@internal", "traefik.http.routers.traefik.service=api@internal",
"traefik.http.services.traefik.loadbalancer.server.port=8080", "traefik.http.services.traefik.loadbalancer.server.port=8080",
"coolify.managed=true",
]; ];
$config = [ $config = [
"version" => "3.8", "version" => "3.8",

View File

@@ -228,56 +228,6 @@ function refresh_server_connection(?PrivateKey $private_key = null)
} }
} }
// function validateServer(Server $server, bool $throwError = false)
// {
// try {
// $uptime = instant_remote_process(['uptime'], $server, $throwError);
// if (!$uptime) {
// $server->settings->is_reachable = false;
// $server->team->notify(new Unreachable($server));
// $server->unreachable_notification_sent = true;
// $server->save();
// return [
// "uptime" => null,
// "dockerVersion" => null,
// ];
// }
// $server->settings->is_reachable = true;
// instant_remote_process(["docker ps"], $server, $throwError);
// $dockerVersion = instant_remote_process(["docker version|head -2|grep -i version| awk '{print $2}'"], $server, $throwError);
// if (!$dockerVersion) {
// $dockerVersion = null;
// return [
// "uptime" => $uptime,
// "dockerVersion" => null,
// ];
// }
// $dockerVersion = checkMinimumDockerEngineVersion($dockerVersion);
// if (is_null($dockerVersion)) {
// $server->settings->is_usable = false;
// } else {
// $server->settings->is_usable = true;
// if (data_get($server, 'unreachable_notification_sent') === true) {
// $server->team->notify(new Revived($server));
// $server->unreachable_notification_sent = false;
// $server->save();
// }
// }
// return [
// "uptime" => $uptime,
// "dockerVersion" => $dockerVersion,
// ];
// } catch (\Throwable $e) {
// $server->settings->is_reachable = false;
// $server->settings->is_usable = false;
// throw $e;
// } finally {
// if (data_get($server, 'settings')) {
// $server->settings->save();
// }
// }
// }
function checkRequiredCommands(Server $server) function checkRequiredCommands(Server $server)
{ {
$commands = collect(["jq", "jc"]); $commands = collect(["jq", "jc"]);

View File

@@ -21,49 +21,6 @@ function replaceVariables($variable)
return $variable->replaceFirst('$', '')->replaceFirst('{', '')->replaceLast('}', ''); return $variable->replaceFirst('$', '')->replaceFirst('{', '')->replaceLast('}', '');
} }
function serviceStatus(Service $service)
{
$foundRunning = false;
$isDegraded = false;
$foundRestaring = false;
$applications = $service->applications;
$databases = $service->databases;
foreach ($applications as $application) {
if ($application->exclude_from_status) {
continue;
}
if (Str::of($application->status)->startsWith('running')) {
$foundRunning = true;
} else if (Str::of($application->status)->startsWith('restarting')) {
$foundRestaring = true;
} else {
$isDegraded = true;
}
}
foreach ($databases as $database) {
if ($database->exclude_from_status) {
continue;
}
if (Str::of($database->status)->startsWith('running')) {
$foundRunning = true;
} else if (Str::of($database->status)->startsWith('restarting')) {
$foundRestaring = true;
} else {
$isDegraded = true;
}
}
if ($foundRestaring) {
return 'degraded';
}
if ($foundRunning && !$isDegraded) {
return 'running';
} else if ($foundRunning && $isDegraded) {
return 'degraded';
} else if (!$foundRunning && !$isDegraded) {
return 'exited';
}
return 'exited';
}
function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase $oneService, bool $isInit = false) function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase $oneService, bool $isInit = false)
{ {
// TODO: make this async // TODO: make this async

View File

@@ -104,7 +104,7 @@ function handleError(?Throwable $error = null, ?Livewire\Component $livewire = n
ray($error); ray($error);
if ($error instanceof TooManyRequestsException) { if ($error instanceof TooManyRequestsException) {
if (isset($livewire)) { if (isset($livewire)) {
return $livewire->dispatch('error', "Too many requests.","Please try again in {$error->secondsUntilAvailable} seconds."); return $livewire->dispatch('error', "Too many requests.", "Please try again in {$error->secondsUntilAvailable} seconds.");
} }
return "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds."; return "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds.";
} }
@@ -125,6 +125,9 @@ function handleError(?Throwable $error = null, ?Livewire\Component $livewire = n
} }
if (isset($livewire)) { if (isset($livewire)) {
if (str($message)->length() > 20) {
return $livewire->dispatch('error', 'Error occured', $message);
}
return $livewire->dispatch('error', $message); return $livewire->dispatch('error', $message);
} }
throw new Exception($message); throw new Exception($message);
@@ -527,28 +530,32 @@ function getTopLevelNetworks(Service|Application $resource)
$definedNetwork = collect([$resource->uuid]); $definedNetwork = collect([$resource->uuid]);
$services = collect($services)->map(function ($service, $_) use ($topLevelNetworks, $definedNetwork) { $services = collect($services)->map(function ($service, $_) use ($topLevelNetworks, $definedNetwork) {
$serviceNetworks = collect(data_get($service, 'networks', [])); $serviceNetworks = collect(data_get($service, 'networks', []));
$hasHostNetworkMode = data_get($service, 'network_mode') === 'host' ? true : false;
// Collect/create/update networks // Only add 'networks' key if 'network_mode' is not 'host'
if ($serviceNetworks->count() > 0) { if (!$hasHostNetworkMode) {
foreach ($serviceNetworks as $networkName => $networkDetails) { // Collect/create/update networks
$networkExists = $topLevelNetworks->contains(function ($value, $key) use ($networkName) { if ($serviceNetworks->count() > 0) {
return $value == $networkName || $key == $networkName; foreach ($serviceNetworks as $networkName => $networkDetails) {
}); $networkExists = $topLevelNetworks->contains(function ($value, $key) use ($networkName) {
if (!$networkExists) { return $value == $networkName || $key == $networkName;
$topLevelNetworks->put($networkDetails, null); });
if (!$networkExists) {
$topLevelNetworks->put($networkDetails, null);
}
} }
} }
}
$definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) { $definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) {
return $value == $definedNetwork; return $value == $definedNetwork;
}); });
if (!$definedNetworkExists) { if (!$definedNetworkExists) {
foreach ($definedNetwork as $network) { foreach ($definedNetwork as $network) {
$topLevelNetworks->put($network, [ $topLevelNetworks->put($network, [
'name' => $network, 'name' => $network,
'external' => true 'external' => true
]); ]);
}
} }
} }
@@ -628,6 +635,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$serviceNetworks = collect(data_get($service, 'networks', [])); $serviceNetworks = collect(data_get($service, 'networks', []));
$serviceVariables = collect(data_get($service, 'environment', [])); $serviceVariables = collect(data_get($service, 'environment', []));
$serviceLabels = collect(data_get($service, 'labels', [])); $serviceLabels = collect(data_get($service, 'labels', []));
$hasHostNetworkMode = data_get($service, 'network_mode') === 'host' ? true : false;
if ($serviceLabels->count() > 0) { if ($serviceLabels->count() > 0) {
$removedLabels = collect([]); $removedLabels = collect([]);
$serviceLabels = $serviceLabels->filter(function ($serviceLabel, $serviceLabelName) use ($removedLabels) { $serviceLabels = $serviceLabels->filter(function ($serviceLabel, $serviceLabelName) use ($removedLabels) {
@@ -698,7 +706,6 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$savedService->image = $image; $savedService->image = $image;
$savedService->save(); $savedService->save();
} }
// Collect/create/update networks // Collect/create/update networks
if ($serviceNetworks->count() > 0) { if ($serviceNetworks->count() > 0) {
foreach ($serviceNetworks as $networkName => $networkDetails) { foreach ($serviceNetworks as $networkName => $networkDetails) {
@@ -729,37 +736,39 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$savedService->ports = $collectedPorts->implode(','); $savedService->ports = $collectedPorts->implode(',');
$savedService->save(); $savedService->save();
// Add Coolify specific networks if (!$hasHostNetworkMode) {
$definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) { // Add Coolify specific networks
return $value == $definedNetwork; $definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) {
}); return $value == $definedNetwork;
if (!$definedNetworkExists) { });
foreach ($definedNetwork as $network) { if (!$definedNetworkExists) {
$topLevelNetworks->put($network, [ foreach ($definedNetwork as $network) {
'name' => $network, $topLevelNetworks->put($network, [
'external' => true 'name' => $network,
]); 'external' => true
]);
}
} }
} $networks = collect();
$networks = collect(); foreach ($serviceNetworks as $key => $serviceNetwork) {
foreach ($serviceNetworks as $key => $serviceNetwork) { if (gettype($serviceNetwork) === 'string') {
if (gettype($serviceNetwork) === 'string') { // networks:
// networks: // - appwrite
// - appwrite $networks->put($serviceNetwork, null);
$networks->put($serviceNetwork, null); } else if (gettype($serviceNetwork) === 'array') {
} else if (gettype($serviceNetwork) === 'array') { // networks:
// networks: // default:
// default: // ipv4_address: 192.168.203.254
// ipv4_address: 192.168.203.254 // $networks->put($serviceNetwork, null);
// $networks->put($serviceNetwork, null); ray($key);
ray($key); $networks->put($key, $serviceNetwork);
$networks->put($key, $serviceNetwork); }
} }
foreach ($definedNetwork as $key => $network) {
$networks->put($network, null);
}
data_set($service, 'networks', $networks->toArray());
} }
foreach ($definedNetwork as $key => $network) {
$networks->put($network, null);
}
data_set($service, 'networks', $networks->toArray());
// Collect/create/update volumes // Collect/create/update volumes
if ($serviceVolumes->count() > 0) { if ($serviceVolumes->count() > 0) {
@@ -782,14 +791,14 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$source = data_get_str($volume, 'source'); $source = data_get_str($volume, 'source');
$target = data_get_str($volume, 'target'); $target = data_get_str($volume, 'target');
$content = data_get($volume, 'content'); $content = data_get($volume, 'content');
$isDirectory = (bool) data_get($volume, 'isDirectory', false); $isDirectory = (bool) data_get($volume, 'isDirectory', false) || (bool) data_get($volume, 'is_directory', false);
$foundConfig = $savedService->fileStorages()->whereMountPath($target)->first(); $foundConfig = $savedService->fileStorages()->whereMountPath($target)->first();
if ($foundConfig) { if ($foundConfig) {
$contentNotNull = data_get($foundConfig, 'content'); $contentNotNull = data_get($foundConfig, 'content');
if ($contentNotNull) { if ($contentNotNull) {
$content = $contentNotNull; $content = $contentNotNull;
} }
$isDirectory = (bool) data_get($foundConfig, 'is_directory'); $isDirectory = (bool) data_get($volume, 'isDirectory', false) || (bool) data_get($volume, 'is_directory', false);
} }
} }
if ($type->value() === 'bind') { if ($type->value() === 'bind') {
@@ -1030,7 +1039,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$serviceLabels = $serviceLabels->merge($defaultLabels); $serviceLabels = $serviceLabels->merge($defaultLabels);
if (!$isDatabase && $fqdns->count() > 0) { if (!$isDatabase && $fqdns->count() > 0) {
if ($fqdns) { if ($fqdns) {
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($resource->uuid, $fqdns, true)); $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($resource->uuid, $fqdns, true, serviceLabels: $serviceLabels, is_gzip_enabled: $savedService->isGzipEnabled()));
} }
} }
if ($resource->server->isLogDrainEnabled() && $savedService->isLogDrainEnabled()) { if ($resource->server->isLogDrainEnabled() && $savedService->isLogDrainEnabled()) {
@@ -1054,6 +1063,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
data_set($service, 'container_name', $containerName); data_set($service, 'container_name', $containerName);
data_forget($service, 'volumes.*.content'); data_forget($service, 'volumes.*.content');
data_forget($service, 'volumes.*.isDirectory'); data_forget($service, 'volumes.*.isDirectory');
data_forget($service, 'volumes.*.is_directory');
// Remove unnecessary variables from service.environment // Remove unnecessary variables from service.environment
// $withoutServiceEnvs = collect([]); // $withoutServiceEnvs = collect([]);
// collect(data_get($service, 'environment'))->each(function ($value, $key) use ($withoutServiceEnvs) { // collect(data_get($service, 'environment'))->each(function ($value, $key) use ($withoutServiceEnvs) {
@@ -1469,7 +1480,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
return $preview_fqdn; return $preview_fqdn;
}); });
} }
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($uuid, $fqdns)); $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($uuid, $fqdns,serviceLabels: $serviceLabels));
} }
} }
} }
@@ -1690,7 +1701,7 @@ function check_fqdn_usage(ServiceApplication|Application $own_resource)
$naked_domain = str($domain)->replace('http://', '')->replace('https://', '')->value(); $naked_domain = str($domain)->replace('http://', '')->replace('https://', '')->value();
if ($domains->contains($naked_domain)) { if ($domains->contains($naked_domain)) {
if ($app->uuid !== $own_resource->uuid) { if ($app->uuid !== $own_resource->uuid) {
throw new \RuntimeException("Domain $naked_domain is already in use by another resource."); throw new \RuntimeException("Domain $naked_domain is already in use by another resource:<br> {$app->name}.");
} }
} }
} }

803
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -65,7 +65,7 @@ return [
'driver' => 'redis', 'driver' => 'redis',
'connection' => 'default', 'connection' => 'default',
'queue' => env('REDIS_QUEUE', 'default'), 'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => 3600, 'retry_after' => 86400,
'block_for' => null, 'block_for' => null,
'after_commit' => true, 'after_commit' => true,
], ],

View File

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

View File

@@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.211'; return '4.0.0-beta.221';

View File

@@ -24,7 +24,6 @@ return new class extends Migration
$table->string('taggable_type'); $table->string('taggable_type');
$table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade'); $table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade');
$table->unique(['tag_id', 'taggable_id', 'taggable_type'], 'taggable_unique'); // Composite unique index $table->unique(['tag_id', 'taggable_id', 'taggable_type'], 'taggable_unique'); // Composite unique index
}); });
} }

View File

@@ -0,0 +1,37 @@
<?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::create('additional_destinations', function (Blueprint $table) {
$table->id();
$table->foreignId('application_id')->constrained()->onDelete('cascade');
$table->foreignId('server_id')->constrained()->onDelete('cascade');
$table->string('status')->default('exited');
$table->foreignId('standalone_docker_id')->constrained()->onDelete('cascade');
$table->timestamps();
});
Schema::table('applications', function (Blueprint $table) {
$table->dropColumn('additional_destinations');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('additional_destinations');
Schema::table('applications', function (Blueprint $table) {
$table->string('additional_destinations')->nullable()->after('destination');
});
}
};

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('server_settings', function (Blueprint $table) {
$table->integer('dynamic_timeout')->default(3600);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('server_settings', function (Blueprint $table) {
$table->dropColumn('dynamic_timeout');
});
}
};

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_consistent_container_name_enabled')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('application_settings', function (Blueprint $table) {
$table->dropColumn('is_consistent_container_name_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('service_applications', function (Blueprint $table) {
$table->boolean('is_gzip_enabled')->default(true);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('service_applications', function (Blueprint $table) {
$table->dropColumn('is_gzip_enabled');
});
}
};

View File

@@ -1,7 +1,7 @@
version: '3.8' version: '3.8'
services: services:
coolify: coolify:
image: "ghcr.io/coollabsio/coolify:${LATEST_IMAGE:latest}" image: "ghcr.io/coollabsio/coolify:${LATEST_IMAGE:-latest}"
volumes: volumes:
- type: bind - type: bind
source: /data/coolify/source/.env source: /data/coolify/source/.env

View File

@@ -1,3 +1,3 @@
#!/command/execlineb -P #!/command/execlineb -P
s6-setuidgid webuser s6-setuidgid webuser
php /var/www/html/artisan app:init --cleanup php /var/www/html/artisan app:init --full-cleanup

176
package-lock.json generated
View File

@@ -7,7 +7,7 @@
"dependencies": { "dependencies": {
"@tailwindcss/typography": "0.5.10", "@tailwindcss/typography": "0.5.10",
"alpinejs": "3.13.5", "alpinejs": "3.13.5",
"daisyui": "4.4.19", "daisyui": "4.7.2",
"ioredis": "5.3.2", "ioredis": "5.3.2",
"tailwindcss-scrollbar": "0.1.0" "tailwindcss-scrollbar": "0.1.0"
}, },
@@ -17,11 +17,11 @@
"axios": "1.6.7", "axios": "1.6.7",
"laravel-echo": "1.15.3", "laravel-echo": "1.15.3",
"laravel-vite-plugin": "0.8.1", "laravel-vite-plugin": "0.8.1",
"postcss": "8.4.33", "postcss": "8.4.35",
"pusher-js": "8.4.0-rc2", "pusher-js": "8.4.0-rc2",
"tailwindcss": "3.4.1", "tailwindcss": "3.4.1",
"vite": "4.5.2", "vite": "4.5.2",
"vue": "3.4.15" "vue": "3.4.19"
} }
}, },
"node_modules/@alloc/quick-lru": { "node_modules/@alloc/quick-lru": {
@@ -524,77 +524,77 @@
} }
}, },
"node_modules/@vue/compiler-core": { "node_modules/@vue/compiler-core": {
"version": "3.4.15", "version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.15.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.19.tgz",
"integrity": "sha512-XcJQVOaxTKCnth1vCxEChteGuwG6wqnUHxAm1DO3gCz0+uXKaJNx8/digSz4dLALCy8n2lKq24jSUs8segoqIw==", "integrity": "sha512-gj81785z0JNzRcU0Mq98E56e4ltO1yf8k5PQ+tV/7YHnbZkrM0fyFyuttnN8ngJZjbpofWE/m4qjKBiLl8Ju4w==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/parser": "^7.23.6", "@babel/parser": "^7.23.9",
"@vue/shared": "3.4.15", "@vue/shared": "3.4.19",
"entities": "^4.5.0", "entities": "^4.5.0",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"source-map-js": "^1.0.2" "source-map-js": "^1.0.2"
} }
}, },
"node_modules/@vue/compiler-core/node_modules/@vue/shared": { "node_modules/@vue/compiler-core/node_modules/@vue/shared": {
"version": "3.4.15", "version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.15.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.19.tgz",
"integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g==", "integrity": "sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw==",
"dev": true "dev": true
}, },
"node_modules/@vue/compiler-dom": { "node_modules/@vue/compiler-dom": {
"version": "3.4.15", "version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.15.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.19.tgz",
"integrity": "sha512-wox0aasVV74zoXyblarOM3AZQz/Z+OunYcIHe1OsGclCHt8RsRm04DObjefaI82u6XDzv+qGWZ24tIsRAIi5MQ==", "integrity": "sha512-vm6+cogWrshjqEHTzIDCp72DKtea8Ry/QVpQRYoyTIg9k7QZDX6D8+HGURjtmatfgM8xgCFtJJaOlCaRYRK3QA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@vue/compiler-core": "3.4.15", "@vue/compiler-core": "3.4.19",
"@vue/shared": "3.4.15" "@vue/shared": "3.4.19"
} }
}, },
"node_modules/@vue/compiler-dom/node_modules/@vue/shared": { "node_modules/@vue/compiler-dom/node_modules/@vue/shared": {
"version": "3.4.15", "version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.15.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.19.tgz",
"integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g==", "integrity": "sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw==",
"dev": true "dev": true
}, },
"node_modules/@vue/compiler-sfc": { "node_modules/@vue/compiler-sfc": {
"version": "3.4.15", "version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.15.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.19.tgz",
"integrity": "sha512-LCn5M6QpkpFsh3GQvs2mJUOAlBQcCco8D60Bcqmf3O3w5a+KWS5GvYbrrJBkgvL1BDnTp+e8q0lXCLgHhKguBA==", "integrity": "sha512-LQ3U4SN0DlvV0xhr1lUsgLCYlwQfUfetyPxkKYu7dkfvx7g3ojrGAkw0AERLOKYXuAGnqFsEuytkdcComei3Yg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/parser": "^7.23.6", "@babel/parser": "^7.23.9",
"@vue/compiler-core": "3.4.15", "@vue/compiler-core": "3.4.19",
"@vue/compiler-dom": "3.4.15", "@vue/compiler-dom": "3.4.19",
"@vue/compiler-ssr": "3.4.15", "@vue/compiler-ssr": "3.4.19",
"@vue/shared": "3.4.15", "@vue/shared": "3.4.19",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"magic-string": "^0.30.5", "magic-string": "^0.30.6",
"postcss": "^8.4.33", "postcss": "^8.4.33",
"source-map-js": "^1.0.2" "source-map-js": "^1.0.2"
} }
}, },
"node_modules/@vue/compiler-sfc/node_modules/@vue/shared": { "node_modules/@vue/compiler-sfc/node_modules/@vue/shared": {
"version": "3.4.15", "version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.15.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.19.tgz",
"integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g==", "integrity": "sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw==",
"dev": true "dev": true
}, },
"node_modules/@vue/compiler-ssr": { "node_modules/@vue/compiler-ssr": {
"version": "3.4.15", "version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.15.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.19.tgz",
"integrity": "sha512-1jdeQyiGznr8gjFDadVmOJqZiLNSsMa5ZgqavkPZ8O2wjHv0tVuAEsw5hTdUoUW4232vpBbL/wJhzVW/JwY1Uw==", "integrity": "sha512-P0PLKC4+u4OMJ8sinba/5Z/iDT84uMRRlrWzadgLA69opCpI1gG4N55qDSC+dedwq2fJtzmGald05LWR5TFfLw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@vue/compiler-dom": "3.4.15", "@vue/compiler-dom": "3.4.19",
"@vue/shared": "3.4.15" "@vue/shared": "3.4.19"
} }
}, },
"node_modules/@vue/compiler-ssr/node_modules/@vue/shared": { "node_modules/@vue/compiler-ssr/node_modules/@vue/shared": {
"version": "3.4.15", "version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.15.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.19.tgz",
"integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g==", "integrity": "sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw==",
"dev": true "dev": true
}, },
"node_modules/@vue/reactivity": { "node_modules/@vue/reactivity": {
@@ -606,64 +606,64 @@
} }
}, },
"node_modules/@vue/runtime-core": { "node_modules/@vue/runtime-core": {
"version": "3.4.15", "version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.15.tgz", "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.19.tgz",
"integrity": "sha512-6E3by5m6v1AkW0McCeAyhHTw+3y17YCOKG0U0HDKDscV4Hs0kgNT5G+GCHak16jKgcCDHpI9xe5NKb8sdLCLdw==", "integrity": "sha512-/Z3tFwOrerJB/oyutmJGoYbuoadphDcJAd5jOuJE86THNZji9pYjZroQ2NFsZkTxOq0GJbb+s2kxTYToDiyZzw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@vue/reactivity": "3.4.15", "@vue/reactivity": "3.4.19",
"@vue/shared": "3.4.15" "@vue/shared": "3.4.19"
} }
}, },
"node_modules/@vue/runtime-core/node_modules/@vue/reactivity": { "node_modules/@vue/runtime-core/node_modules/@vue/reactivity": {
"version": "3.4.15", "version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.15.tgz", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.19.tgz",
"integrity": "sha512-55yJh2bsff20K5O84MxSvXKPHHt17I2EomHznvFiJCAZpJTNW8IuLj1xZWMLELRhBK3kkFV/1ErZGHJfah7i7w==", "integrity": "sha512-+VcwrQvLZgEclGZRHx4O2XhyEEcKaBi50WbxdVItEezUf4fqRh838Ix6amWTdX0CNb/b6t3Gkz3eOebfcSt+UA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@vue/shared": "3.4.15" "@vue/shared": "3.4.19"
} }
}, },
"node_modules/@vue/runtime-core/node_modules/@vue/shared": { "node_modules/@vue/runtime-core/node_modules/@vue/shared": {
"version": "3.4.15", "version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.15.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.19.tgz",
"integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g==", "integrity": "sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw==",
"dev": true "dev": true
}, },
"node_modules/@vue/runtime-dom": { "node_modules/@vue/runtime-dom": {
"version": "3.4.15", "version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.15.tgz", "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.19.tgz",
"integrity": "sha512-EVW8D6vfFVq3V/yDKNPBFkZKGMFSvZrUQmx196o/v2tHKdwWdiZjYUBS+0Ez3+ohRyF8Njwy/6FH5gYJ75liUw==", "integrity": "sha512-IyZzIDqfNCF0OyZOauL+F4yzjMPN2rPd8nhqPP2N1lBn3kYqJpPHHru+83Rkvo2lHz5mW+rEeIMEF9qY3PB94g==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@vue/runtime-core": "3.4.15", "@vue/runtime-core": "3.4.19",
"@vue/shared": "3.4.15", "@vue/shared": "3.4.19",
"csstype": "^3.1.3" "csstype": "^3.1.3"
} }
}, },
"node_modules/@vue/runtime-dom/node_modules/@vue/shared": { "node_modules/@vue/runtime-dom/node_modules/@vue/shared": {
"version": "3.4.15", "version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.15.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.19.tgz",
"integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g==", "integrity": "sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw==",
"dev": true "dev": true
}, },
"node_modules/@vue/server-renderer": { "node_modules/@vue/server-renderer": {
"version": "3.4.15", "version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.15.tgz", "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.19.tgz",
"integrity": "sha512-3HYzaidu9cHjrT+qGUuDhFYvF/j643bHC6uUN9BgM11DVy+pM6ATsG6uPBLnkwOgs7BpJABReLmpL3ZPAsUaqw==", "integrity": "sha512-eAj2p0c429RZyyhtMRnttjcSToch+kTWxFPHlzGMkR28ZbF1PDlTcmGmlDxccBuqNd9iOQ7xPRPAGgPVj+YpQw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@vue/compiler-ssr": "3.4.15", "@vue/compiler-ssr": "3.4.19",
"@vue/shared": "3.4.15" "@vue/shared": "3.4.19"
}, },
"peerDependencies": { "peerDependencies": {
"vue": "3.4.15" "vue": "3.4.19"
} }
}, },
"node_modules/@vue/server-renderer/node_modules/@vue/shared": { "node_modules/@vue/server-renderer/node_modules/@vue/shared": {
"version": "3.4.15", "version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.15.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.19.tgz",
"integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g==", "integrity": "sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw==",
"dev": true "dev": true
}, },
"node_modules/@vue/shared": { "node_modules/@vue/shared": {
@@ -953,9 +953,9 @@
} }
}, },
"node_modules/daisyui": { "node_modules/daisyui": {
"version": "4.4.19", "version": "4.7.2",
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.4.19.tgz", "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.7.2.tgz",
"integrity": "sha512-IjOLWwnndD4N7Ut5CDxbUsaVtbqXPeVHM92IcgxGFxpuOd3CCKW/PAXZH6JoBTHFRaN57vB9XqEhdWm5yC+bPA==", "integrity": "sha512-9UCss12Zmyk/22u+JbkVrHHxOzFOyY17HuqP5LeswI4hclbj6qbjJTovdj2zRy8cCH6/n6Wh0lTLjriGnyGh0g==",
"dependencies": { "dependencies": {
"css-selector-tokenizer": "^0.8", "css-selector-tokenizer": "^0.8",
"culori": "^3", "culori": "^3",
@@ -1410,9 +1410,9 @@
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
}, },
"node_modules/magic-string": { "node_modules/magic-string": {
"version": "0.30.5", "version": "0.30.7",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz",
"integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", "integrity": "sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15" "@jridgewell/sourcemap-codec": "^1.4.15"
@@ -1598,9 +1598,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.33", "version": "8.4.35",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz",
"integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -2106,16 +2106,16 @@
} }
}, },
"node_modules/vue": { "node_modules/vue": {
"version": "3.4.15", "version": "3.4.19",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.4.15.tgz", "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.19.tgz",
"integrity": "sha512-jC0GH4KkWLWJOEQjOpkqU1bQsBwf4R1rsFtw5GQJbjHVKWDzO6P0nWWBTmjp1xSemAioDFj1jdaK1qa3DnMQoQ==", "integrity": "sha512-W/7Fc9KUkajFU8dBeDluM4sRGc/aa4YJnOYck8dkjgZoXtVsn3OeTGni66FV1l3+nvPA7VBFYtPioaGKUmEADw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@vue/compiler-dom": "3.4.15", "@vue/compiler-dom": "3.4.19",
"@vue/compiler-sfc": "3.4.15", "@vue/compiler-sfc": "3.4.19",
"@vue/runtime-dom": "3.4.15", "@vue/runtime-dom": "3.4.19",
"@vue/server-renderer": "3.4.15", "@vue/server-renderer": "3.4.19",
"@vue/shared": "3.4.15" "@vue/shared": "3.4.19"
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "*" "typescript": "*"
@@ -2127,9 +2127,9 @@
} }
}, },
"node_modules/vue/node_modules/@vue/shared": { "node_modules/vue/node_modules/@vue/shared": {
"version": "3.4.15", "version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.15.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.19.tgz",
"integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g==", "integrity": "sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw==",
"dev": true "dev": true
}, },
"node_modules/wrappy": { "node_modules/wrappy": {

View File

@@ -11,16 +11,16 @@
"axios": "1.6.7", "axios": "1.6.7",
"laravel-echo": "1.15.3", "laravel-echo": "1.15.3",
"laravel-vite-plugin": "0.8.1", "laravel-vite-plugin": "0.8.1",
"postcss": "8.4.33", "postcss": "8.4.35",
"pusher-js": "8.4.0-rc2", "pusher-js": "8.4.0-rc2",
"tailwindcss": "3.4.1", "tailwindcss": "3.4.1",
"vite": "4.5.2", "vite": "4.5.2",
"vue": "3.4.15" "vue": "3.4.19"
}, },
"dependencies": { "dependencies": {
"@tailwindcss/typography": "0.5.10", "@tailwindcss/typography": "0.5.10",
"alpinejs": "3.13.5", "alpinejs": "3.13.5",
"daisyui": "4.4.19", "daisyui": "4.7.2",
"ioredis": "5.3.2", "ioredis": "5.3.2",
"tailwindcss-scrollbar": "0.1.0" "tailwindcss-scrollbar": "0.1.0"
} }

View File

@@ -28,8 +28,8 @@
<a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white" <a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
target="_blank" href="{{ getFqdnWithoutPort($domain) }}"> target="_blank" href="{{ getFqdnWithoutPort($domain) }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-width="1.5" stroke="currentColor" fill="none"
stroke-linejoin="round"> stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 15l6 -6" /> <path d="M9 15l6 -6" />
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" /> <path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
@@ -84,7 +84,7 @@
@endif @endif
@if (data_get($application, 'ports_mappings_array')) @if (data_get($application, 'ports_mappings_array'))
@foreach ($application->ports_mappings_array as $port) @foreach ($application->ports_mappings_array as $port)
@if (isDev()) @if ($application->destination->server->id === 0)
<li> <li>
<a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white" <a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
target="_blank" href="http://localhost:{{ explode(':', $port)[0] }}"> target="_blank" href="http://localhost:{{ explode(':', $port)[0] }}">
@@ -114,9 +114,29 @@
<path <path
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" /> d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
</svg> </svg>
Port {{ $port }} {{ $application->destination->server->ip }}:{{ explode(':', $port)[0] }}
</a> </a>
</li> </li>
@if (count($application->additional_servers) > 0)
@foreach ($application->additional_servers as $server)
<li>
<a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
target="_blank"
href="http://{{ $server->ip }}:{{ explode(':', $port)[0] }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" 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 15l6 -6" />
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
<path
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
</svg>
{{ $server->ip }}:{{ explode(':', $port)[0] }}
</a>
</li>
@endforeach
@endif
@endif @endif
@endforeach @endforeach
@endif @endif

View File

@@ -22,7 +22,7 @@
</a> </a>
@endif @endif
<div class="flex-1"></div> <div class="flex-1"></div>
@if ($database->status !== 'exited') @if (!str($database->status)->startsWith('exited'))
<button wire:click='stop' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400"> <button wire:click='stop' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24" stroke-width="2" <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">

View File

@@ -22,7 +22,7 @@
</svg> </svg>
</div> </div>
@endif @endif
<input value="{{ $value }}" {{ $attributes->merge(['class' => $defaultClass . ' pl-10']) }} <input value="{{ $value }}" {{ $attributes->merge(['class' => $defaultClass]) }}
@required($required) @if ($id !== 'null') wire:model={{ $id }} @endif @required($required) @if ($id !== 'null') wire:model={{ $id }} @endif
wire:dirty.class.remove='text-white' wire:dirty.class="input-warning" wire:loading.attr="disabled" wire:dirty.class.remove='text-white' wire:dirty.class="input-warning" wire:loading.attr="disabled"
type="{{ $type }}" @readonly($readonly) @disabled($disabled) id="{{ $id }}" type="{{ $type }}" @readonly($readonly) @disabled($disabled) id="{{ $id }}"

View File

@@ -28,21 +28,16 @@
<div class="justify-center" wire:click="help" onclick="help.showModal()"> <div class="justify-center" wire:click="help" onclick="help.showModal()">
<svg class="icon" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg"> <svg class="icon" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" <path fill="currentColor"
d="M144 180a16 16 0 1 1-16-16a16 16 0 0 1 16 16m92-52A108 108 0 1 1 128 20a108.12 108.12 0 0 1 108 108m-24 0a84 84 0 1 0-84 84a84.09 84.09 0 0 0 84-84m-84-64c-24.26 0-44 17.94-44 40v4a12 12 0 0 0 24 0v-4c0-8.82 9-16 20-16s20 7.18 20 16s-9 16-20 16a12 12 0 0 0-12 12v8a12 12 0 0 0 23.73 2.56C158.31 137.88 172 122.37 172 104c0-22.06-19.74-40-44-40" /> d="M140 180a12 12 0 1 1-12-12a12 12 0 0 1 12 12M128 72c-22.06 0-40 16.15-40 36v4a8 8 0 0 0 16 0v-4c0-11 10.77-20 24-20s24 9 24 20s-10.77 20-24 20a8 8 0 0 0-8 8v8a8 8 0 0 0 16 0v-.72c18.24-3.35 32-17.9 32-35.28c0-19.85-17.94-36-40-36m104 56A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88" />
</svg> </svg>
</div> </div>
</li> </li>
<li class="pb-6" title="Logout"> <li class="pb-6" title="Logout">
<form action="/logout" method="POST" class=" hover:bg-transparent"> <form action="/logout" method="POST" class="hover:bg-transparent">
@csrf @csrf
<button class="flex items-center gap-2 rounded-none hover:text-white hover:bg-transparent"> <button class="flex items-center gap-2 rounded-none hover:text-white hover:bg-transparent">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" stroke-width="1.5" <svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path fill="currentColor" d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2a9.985 9.985 0 0 1 8 4h-2.71a8 8 0 1 0 .001 12h2.71A9.985 9.985 0 0 1 12 22m7-6v-3h-8v-2h8V8l5 4z"/>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M13 12v.01" />
<path d="M3 21h18" />
<path d="M5 21v-16a2 2 0 0 1 2 -2h7.5m2.5 10.5v7.5" />
<path d="M14 7h7m-3 -3l3 3l-3 3" />
</svg> </svg>
</button> </button>
</form> </form>

View File

@@ -1,5 +1,5 @@
@auth @auth
<nav class="fixed h-full overflow-hidden overflow-y-auto pt-28 scrollbar"> <nav class="fixed h-full pt-28 scrollbar">
<a href="/" class="fixed top-0 z-50 mx-3 mt-3 bg-transparent cursor-pointer"><img <a href="/" class="fixed top-0 z-50 mx-3 mt-3 bg-transparent cursor-pointer"><img
class="transition rounded w-11 h-11" src="{{ asset('coolify-transparent.png') }}"></a> class="transition rounded w-11 h-11" src="{{ asset('coolify-transparent.png') }}"></a>
<ul class="flex flex-col h-full gap-4 menu flex-nowrap"> <ul class="flex flex-col h-full gap-4 menu flex-nowrap">
@@ -40,62 +40,142 @@
</svg> </svg>
</a> </a>
</li> </li>
<li title="Command Center"> <div class="inline-block text-left " x-data="{ open: false }">
<a class="hover:bg-transparent" href="/command-center"> <div>
<svg xmlns="http://www.w3.org/2000/svg" <button x-on:click.prevent="open = !open" x-on:click.away="open = false" type="button"
class="{{ request()->is('command-center') ? 'text-warning icon' : 'icon' }}" viewBox="0 0 24 24" class="py-4 mx-4" id="menu-button" aria-expanded="true" aria-haspopup="true">
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" <svg class="icon" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
stroke-linejoin="round"> <path fill="currentColor"
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> d="M224 128a8 8 0 0 1-8 8h-80v80a8 8 0 0 1-16 0v-80H40a8 8 0 0 1 0-16h80V40a8 8 0 0 1 16 0v80h80a8 8 0 0 1 8 8" />
<path d="M5 7l5 5l-5 5" /> </svg>
<path d="M12 19l7 0" /> </button>
</svg> </div>
</a> <div x-show="open" x-cloak
</li> class="absolute left-0 z-10 w-56 mx-4 mt-2 origin-top-right rounded shadow-lg bg-coolgray-100 ring-1 ring-black ring-opacity-5 focus:outline-none"
<li title="Source"> role="menu" aria-orientation="vertical" aria-labelledby="menu-button" tabindex="-1">
<a class="hover:bg-transparent" href="{{ route('source.all') }}"> <div class="py-1" role="none">
<svg class="icon" viewBox="0 0 15 15" xmlns="http://www.w3.org/2000/svg"> <li title="Tags" class="border-transparent hover:bg-coolgray-200 ">
<path fill="currentColor" <a class=" hover:bg-transparent hover:no-underline" href="{{ route('tags.index') }}">
d="m6.793 1.207l.353.354l-.353-.354ZM1.207 6.793l-.353-.354l.353.354Zm0 1.414l.354-.353l-.354.353Zm5.586 5.586l-.354.353l.354-.353Zm1.414 0l-.353-.354l.353.354Zm5.586-5.586l.353.354l-.353-.354Zm0-1.414l-.354.353l.354-.353ZM8.207 1.207l.354-.353l-.354.353ZM6.44.854L.854 6.439l.707.707l5.585-5.585L6.44.854ZM.854 8.56l5.585 5.585l.707-.707l-5.585-5.585l-.707.707Zm7.707 5.585l5.585-5.585l-.707-.707l-5.585 5.585l.707.707Zm5.585-7.707L8.561.854l-.707.707l5.585 5.585l.707-.707Zm0 2.122a1.5 1.5 0 0 0 0-2.122l-.707.707a.5.5 0 0 1 0 .708l.707.707ZM6.44 14.146a1.5 1.5 0 0 0 2.122 0l-.707-.707a.5.5 0 0 1-.708 0l-.707.707ZM.854 6.44a1.5 1.5 0 0 0 0 2.122l.707-.707a.5.5 0 0 1 0-.708L.854 6.44Zm6.292-4.878a.5.5 0 0 1 .708 0L8.56.854a1.5 1.5 0 0 0-2.122 0l.707.707Zm-2 1.293l1 1l.708-.708l-1-1l-.708.708ZM7.5 5a.5.5 0 0 1-.5-.5H6A1.5 1.5 0 0 0 7.5 6V5Zm.5-.5a.5.5 0 0 1-.5.5v1A1.5 1.5 0 0 0 9 4.5H8ZM7.5 4a.5.5 0 0 1 .5.5h1A1.5 1.5 0 0 0 7.5 3v1Zm0-1A1.5 1.5 0 0 0 6 4.5h1a.5.5 0 0 1 .5-.5V3Zm.646 2.854l1.5 1.5l.707-.708l-1.5-1.5l-.707.708ZM10.5 8a.5.5 0 0 1-.5-.5H9A1.5 1.5 0 0 0 10.5 9V8Zm.5-.5a.5.5 0 0 1-.5.5v1A1.5 1.5 0 0 0 12 7.5h-1Zm-.5-.5a.5.5 0 0 1 .5.5h1A1.5 1.5 0 0 0 10.5 6v1Zm0-1A1.5 1.5 0 0 0 9 7.5h1a.5.5 0 0 1 .5-.5V6ZM7 5.5v4h1v-4H7Zm.5 5.5a.5.5 0 0 1-.5-.5H6A1.5 1.5 0 0 0 7.5 12v-1Zm.5-.5a.5.5 0 0 1-.5.5v1A1.5 1.5 0 0 0 9 10.5H8Zm-.5-.5a.5.5 0 0 1 .5.5h1A1.5 1.5 0 0 0 7.5 9v1Zm0-1A1.5 1.5 0 0 0 6 10.5h1a.5.5 0 0 1 .5-.5V9Z" /> <svg class="{{ request()->is('tags*') ? 'text-warning icon' : 'icon' }}"viewBox="0 0 24 24"
</svg> xmlns="http://www.w3.org/2000/svg">
</a> <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
</li> stroke-width="2">
<li title="Security"> <path
<a class="hover:bg-transparent" href="{{ route('security.private-key.index') }}"> d="M3 8v4.172a2 2 0 0 0 .586 1.414l5.71 5.71a2.41 2.41 0 0 0 3.408 0l3.592-3.592a2.41 2.41 0 0 0 0-3.408l-5.71-5.71A2 2 0 0 0 9.172 6H5a2 2 0 0 0-2 2" />
<svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="m18 19l1.592-1.592a4.82 4.82 0 0 0 0-6.816L15 6m-8 4h-.01" />
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" </g>
stroke-width="2" </svg>
d="m16.555 3.843l3.602 3.602a2.877 2.877 0 0 1 0 4.069l-2.643 2.643a2.877 2.877 0 0 1-4.069 0l-.301-.301l-6.558 6.558a2 2 0 0 1-1.239.578L5.172 21H4a1 1 0 0 1-.993-.883L3 20v-1.172a2 2 0 0 1 .467-1.284l.119-.13L4 17h2v-2h2v-2l2.144-2.144l-.301-.301a2.877 2.877 0 0 1 0-4.069l2.643-2.643a2.877 2.877 0 0 1 4.069 0zM15 9h.01" /> Tags
</svg> </a>
</a> </li>
</li> <li title="Command Center" class="hover:bg-coolgray-200">
<li title="Teams"> <a class="hover:bg-transparent hover:no-underline" href="{{ route('command-center') }}">
<a class="hover:bg-transparent" href="{{ route('team.index') }}"> <svg xmlns="http://www.w3.org/2000/svg"
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" stroke-width="1.5" class="{{ request()->is('command-center*') ? 'text-warning icon' : 'icon' }}"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none"
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> stroke-linecap="round" stroke-linejoin="round">
<path d="M10 13a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M8 21v-1a2 2 0 0 1 2 -2h4a2 2 0 0 1 2 2v1" /> <path d="M5 7l5 5l-5 5" />
<path d="M15 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" /> <path d="M12 19l7 0" />
<path d="M17 10h2a2 2 0 0 1 2 2v1" /> </svg>
<path d="M5 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" /> Command Center
<path d="M3 13v-1a2 2 0 0 1 2 -2h2" /> </a>
</svg> </li>
</a> <li title="Source" class="hover:bg-coolgray-200">
</li> <a class="hover:bg-transparent hover:no-underline" href="{{ route('source.all') }}">
<li title="Tags"> <svg class="{{ request()->is('source*') ? 'text-warning icon' : 'icon' }}"
<a class="hover:bg-transparent" href="{{ route('tags.index') }}"> viewBox="0 0 15 15" xmlns="http://www.w3.org/2000/svg">
<svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path fill="currentColor"
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="m6.793 1.207l.353.354l-.353-.354ZM1.207 6.793l-.353-.354l.353.354Zm0 1.414l.354-.353l-.354.353Zm5.586 5.586l-.354.353l.354-.353Zm1.414 0l-.353-.354l.353.354Zm5.586-5.586l.353.354l-.353-.354Zm0-1.414l-.354.353l.354-.353ZM8.207 1.207l.354-.353l-.354.353ZM6.44.854L.854 6.439l.707.707l5.585-5.585L6.44.854ZM.854 8.56l5.585 5.585l.707-.707l-5.585-5.585l-.707.707Zm7.707 5.585l5.585-5.585l-.707-.707l-5.585 5.585l.707.707Zm5.585-7.707L8.561.854l-.707.707l5.585 5.585l.707-.707Zm0 2.122a1.5 1.5 0 0 0 0-2.122l-.707.707a.5.5 0 0 1 0 .708l.707.707ZM6.44 14.146a1.5 1.5 0 0 0 2.122 0l-.707-.707a.5.5 0 0 1-.708 0l-.707.707ZM.854 6.44a1.5 1.5 0 0 0 0 2.122l.707-.707a.5.5 0 0 1 0-.708L.854 6.44Zm6.292-4.878a.5.5 0 0 1 .708 0L8.56.854a1.5 1.5 0 0 0-2.122 0l.707.707Zm-2 1.293l1 1l.708-.708l-1-1l-.708.708ZM7.5 5a.5.5 0 0 1-.5-.5H6A1.5 1.5 0 0 0 7.5 6V5Zm.5-.5a.5.5 0 0 1-.5.5v1A1.5 1.5 0 0 0 9 4.5H8ZM7.5 4a.5.5 0 0 1 .5.5h1A1.5 1.5 0 0 0 7.5 3v1Zm0-1A1.5 1.5 0 0 0 6 4.5h1a.5.5 0 0 1 .5-.5V3Zm.646 2.854l1.5 1.5l.707-.708l-1.5-1.5l-.707.708ZM10.5 8a.5.5 0 0 1-.5-.5H9A1.5 1.5 0 0 0 10.5 9V8Zm.5-.5a.5.5 0 0 1-.5.5v1A1.5 1.5 0 0 0 12 7.5h-1Zm-.5-.5a.5.5 0 0 1 .5.5h1A1.5 1.5 0 0 0 10.5 6v1Zm0-1A1.5 1.5 0 0 0 9 7.5h1a.5.5 0 0 1 .5-.5V6ZM7 5.5v4h1v-4H7Zm.5 5.5a.5.5 0 0 1-.5-.5H6A1.5 1.5 0 0 0 7.5 12v-1Zm.5-.5a.5.5 0 0 1-.5.5v1A1.5 1.5 0 0 0 9 10.5H8Zm-.5-.5a.5.5 0 0 1 .5.5h1A1.5 1.5 0 0 0 7.5 9v1Zm0-1A1.5 1.5 0 0 0 6 10.5h1a.5.5 0 0 1 .5-.5V9Z" />
stroke-width="2"> </svg>
<path Sources
d="M3 8v4.172a2 2 0 0 0 .586 1.414l5.71 5.71a2.41 2.41 0 0 0 3.408 0l3.592-3.592a2.41 2.41 0 0 0 0-3.408l-5.71-5.71A2 2 0 0 0 9.172 6H5a2 2 0 0 0-2 2" /> </a>
<path d="m18 19l1.592-1.592a4.82 4.82 0 0 0 0-6.816L15 6m-8 4h-.01" /> </li>
</g> <li title="Security" class="hover:bg-coolgray-200">
</svg> <a class="hover:bg-transparent hover:no-underline"
</a> href="{{ route('security.private-key.index') }}">
</li> <svg class="{{ request()->is('security*') ? 'text-warning icon' : 'icon' }}"
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="none" stroke="currentColor" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2"
d="m16.555 3.843l3.602 3.602a2.877 2.877 0 0 1 0 4.069l-2.643 2.643a2.877 2.877 0 0 1-4.069 0l-.301-.301l-6.558 6.558a2 2 0 0 1-1.239.578L5.172 21H4a1 1 0 0 1-.993-.883L3 20v-1.172a2 2 0 0 1 .467-1.284l.119-.13L4 17h2v-2h2v-2l2.144-2.144l-.301-.301a2.877 2.877 0 0 1 0-4.069l2.643-2.643a2.877 2.877 0 0 1 4.069 0zM15 9h.01" />
</svg>
Security
</a>
</li>
<li title="Profile" class="hover:bg-coolgray-200">
<a class="hover:bg-transparent hover:no-underline" href="{{ route('profile') }}">
<svg xmlns="http://www.w3.org/2000/svg"
class="{{ request()->is('profile*') ? 'text-warning icon' : 'icon' }}"
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="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
<path d="M12 10m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0" />
<path d="M6.168 18.849a4 4 0 0 1 3.832 -2.849h4a4 4 0 0 1 3.834 2.855" />
</svg>
Profile
</a>
</li>
<li title="Teams" class="hover:bg-coolgray-200">
<a class="hover:bg-transparent hover:no-underline" href="{{ route('team.index') }}">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
class="{{ request()->is('team*') ? 'text-warning icon' : 'icon' }}"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M10 13a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
<path d="M8 21v-1a2 2 0 0 1 2 -2h4a2 2 0 0 1 2 2v1" />
<path d="M15 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
<path d="M17 10h2a2 2 0 0 1 2 2v1" />
<path d="M5 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
<path d="M3 13v-1a2 2 0 0 1 2 -2h2" />
</svg>
Teams @if (isCloud())
/ Subscription
@endif
</a>
</li>
@if (isInstanceAdmin())
<li title="Settings" class="hover:bg-coolgray-200">
<a class="hover:bg-transparent hover:no-underline" href="/settings">
<svg xmlns="http://www.w3.org/2000/svg"
class="{{ request()->is('settings*') ? 'text-warning icon' : 'icon' }}"
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z" />
<path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" />
</svg>
Settings
</a>
</li>
@endif
<li title="Boarding" class="hover:bg-coolgray-200">
<a class="hover:bg-transparent hover:no-underline" href="{{ route('boarding') }}">
<svg class="{{ request()->is('boarding*') ? 'text-warning icon' : 'icon' }}"
viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor"
d="M224 128a8 8 0 0 1-8 8h-88a8 8 0 0 1 0-16h88a8 8 0 0 1 8 8m-96-56h88a8 8 0 0 0 0-16h-88a8 8 0 0 0 0 16m88 112h-88a8 8 0 0 0 0 16h88a8 8 0 0 0 0-16M82.34 42.34L56 68.69L45.66 58.34a8 8 0 0 0-11.32 11.32l16 16a8 8 0 0 0 11.32 0l32-32a8 8 0 0 0-11.32-11.32m0 64L56 132.69l-10.34-10.35a8 8 0 0 0-11.32 11.32l16 16a8 8 0 0 0 11.32 0l32-32a8 8 0 0 0-11.32-11.32m0 64L56 196.69l-10.34-10.35a8 8 0 0 0-11.32 11.32l16 16a8 8 0 0 0 11.32 0l32-32a8 8 0 0 0-11.32-11.32" />
</svg>
Boarding
</a>
</li>
</div>
</div>
</div>
@if (isCloud() && isInstanceAdmin())
<li title="Admin">
<a class="hover:bg-transparent" href="/admin">
<svg class="text-pink-600 icon" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor"
d="M177.62 159.6a52 52 0 0 1-34 34a12.2 12.2 0 0 1-3.6.55a12 12 0 0 1-3.6-23.45a28 28 0 0 0 18.32-18.32a12 12 0 0 1 22.9 7.2ZM220 144a92 92 0 0 1-184 0c0-28.81 11.27-58.18 33.48-87.28a12 12 0 0 1 17.9-1.33l19.69 19.11L127 19.89a12 12 0 0 1 18.94-5.12C168.2 33.25 220 82.85 220 144m-24 0c0-41.71-30.61-78.39-52.52-99.29l-20.21 55.4a12 12 0 0 1-19.63 4.5L80.71 82.36C67 103.38 60 124.06 60 144a68 68 0 0 0 136 0" />
</svg>
</a>
</li>
@endif
<div class="flex-1"></div> <div class="flex-1"></div>
@if (isInstanceAdmin() && !isCloud()) @if (isInstanceAdmin() && !isCloud())
@persist('upgrade') @persist('upgrade')
@@ -114,58 +194,26 @@
</svg> </svg>
</a> </a>
</li> </li>
<li title="Profile">
<a class="hover:bg-transparent" href="/profile">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" 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="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
<path d="M12 10m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0" />
<path d="M6.168 18.849a4 4 0 0 1 3.832 -2.849h4a4 4 0 0 1 3.834 2.855" />
</svg>
</a>
</li>
@if (isInstanceAdmin()) <li title="Send us feedback or get help!" class="hover:bg-transparent">
<li title="Settings" class="mt-auto"> <div class="justify-center" wire:click="help" onclick="help.showModal()">
<a class="hover:bg-transparent" href="/settings"> <svg class="icon" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<svg xmlns="http://www.w3.org/2000/svg" <path fill="currentColor"
class="{{ request()->is('settings*') ? 'text-warning icon' : 'icon' }}" viewBox="0 0 24 24" d="M140 180a12 12 0 1 1-12-12a12 12 0 0 1 12 12M128 72c-22.06 0-40 16.15-40 36v4a8 8 0 0 0 16 0v-4c0-11 10.77-20 24-20s24 9 24 20s-10.77 20-24 20a8 8 0 0 0-8 8v8a8 8 0 0 0 16 0v-.72c18.24-3.35 32-17.9 32-35.28c0-19.85-17.94-36-40-36m104 56A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88" />
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" </svg>
stroke-linejoin="round"> </div>
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> </li>
<path <form action="/logout" method="POST" class="hover:bg-transparent">
d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z" /> <li title="Logout" class="mb-6 hover:transparent">
<path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" />
</svg>
</a>
</li>
@endif
@if (isSubscriptionActive() || isDev())
<li title="Send us feedback or get help!" class="hover:bg-transparent">
<div class="justify-center" wire:click="help" onclick="help.showModal()">
<svg class="icon" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor"
d="M144 180a16 16 0 1 1-16-16a16 16 0 0 1 16 16m92-52A108 108 0 1 1 128 20a108.12 108.12 0 0 1 108 108m-24 0a84 84 0 1 0-84 84a84.09 84.09 0 0 0 84-84m-84-64c-24.26 0-44 17.94-44 40v4a12 12 0 0 0 24 0v-4c0-8.82 9-16 20-16s20 7.18 20 16s-9 16-20 16a12 12 0 0 0-12 12v8a12 12 0 0 0 23.73 2.56C158.31 137.88 172 122.37 172 104c0-22.06-19.74-40-44-40" />
</svg>
</div>
</li>
@endif
<li class="pb-6" title="Logout">
<form action="/logout" method="POST" class=" hover:bg-transparent">
@csrf @csrf
<button type="submit" class="rounded-none hover:text-white hover:bg-transparent"> <button type="submit" class="rounded-none hover:text-white hover:bg-transparent">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" stroke-width="1.5" <svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path fill="currentColor"
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2a9.985 9.985 0 0 1 8 4h-2.71a8 8 0 1 0 .001 12h2.71A9.985 9.985 0 0 1 12 22m7-6v-3h-8v-2h8V8l5 4z" />
<path d="M13 12v.01" />
<path d="M3 21h18" />
<path d="M5 21v-16a2 2 0 0 1 2 -2h7.5m2.5 10.5v7.5" />
<path d="M14 7h7m-3 -3l3 3l-3 3" />
</svg> </svg>
</button> </button>
</form> </li>
</li> </form>
</ul> </ul>
</nav> </nav>
@endauth @endauth

View File

@@ -18,6 +18,12 @@
]) }}"> ]) }}">
<button>General</button> <button>General</button>
</a> </a>
<a class="{{ request()->routeIs('server.resources') ? 'text-white' : '' }}"
href="{{ route('server.resources', [
'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}">
<button>Resources</button>
</a>
<a class="{{ request()->routeIs('server.private-key') ? 'text-white' : '' }}" <a class="{{ request()->routeIs('server.private-key') ? 'text-white' : '' }}"
href="{{ route('server.private-key', [ href="{{ route('server.private-key', [
'server_uuid' => data_get($parameters, 'server_uuid'), 'server_uuid' => data_get($parameters, 'server_uuid'),

View File

@@ -5,7 +5,7 @@
</a> </a>
<x-services.links /> <x-services.links />
<div class="flex-1"></div> <div class="flex-1"></div>
@if (serviceStatus($service) === 'degraded') @if ($service->status() === 'degraded')
<button wire:click='deploy' onclick="startService.showModal()" <button wire:click='deploy' onclick="startService.showModal()"
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400"> class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg class="w-5 h-5 text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg class="w-5 h-5 text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
@@ -26,7 +26,7 @@
Stop Stop
</button> </button>
@endif @endif
@if (serviceStatus($service) === 'running') @if ($service->status() === 'running')
<button wire:click='restart' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400"> <button wire:click='restart' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg class="w-5 h-5 text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg class="w-5 h-5 text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
@@ -47,7 +47,7 @@
Stop Stop
</button> </button>
@endif @endif
@if (serviceStatus($service) === 'exited') @if ($service->status() === 'exited')
<button wire:click='stop(true)' <button wire:click='stop(true)'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400"> class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg class="w-5 h-5 " viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"> <svg class="w-5 h-5 " viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">

View File

@@ -1,4 +1,4 @@
@props(['closeWithX' => 'false', 'fullScreen' => 'false']) @props(['closeWithX' => false, 'fullScreen' => false])
<div x-data="{ <div x-data="{
slideOverOpen: false slideOverOpen: false
}" class="relative w-auto h-auto"> }" class="relative w-auto h-auto">
@@ -19,7 +19,7 @@
x-transition:leave-start="translate-x-0" x-transition:leave-end="translate-x-full" x-transition:leave-start="translate-x-0" x-transition:leave-end="translate-x-full"
@class([ @class([
'max-w-md w-screen' => !$fullScreen, 'max-w-md w-screen' => !$fullScreen,
'max-w-7xl w-screen' => $fullScreen, 'max-w-4xl w-screen' => $fullScreen,
])> ])>
<div <div
class="flex flex-col h-full py-6 overflow-hidden border-l shadow-lg bg-base-100 border-neutral-800"> class="flex flex-col h-full py-6 overflow-hidden border-l shadow-lg bg-base-100 border-neutral-800">

View File

@@ -1,11 +1,15 @@
@if (Str::of($status)->startsWith('running')) @props(['status', 'showRefreshButton' => true])
@if (str($status)->startsWith('running'))
<x-status.running :status="$status" /> <x-status.running :status="$status" />
@elseif(Str::of($status)->startsWith('restarting') || Str::of($status)->startsWith('starting')) @elseif(str($status)->startsWith('restarting') ||
str($status)->startsWith('starting') ||
str($status)->startsWith('degraded'))
<x-status.restarting :status="$status" /> <x-status.restarting :status="$status" />
@else @else
<x-status.stopped :status="$status" /> <x-status.stopped :status="$status"/>
@endif @endif
@if (!str($status)->contains('exited'))
@if (!str($status)->contains('exited') && $showRefreshButton)
<button title="Refresh Status" wire:click='check_status(true)' class="mx-1 hover:fill-white fill-warning"> <button title="Refresh Status" wire:click='check_status(true)' class="mx-1 hover:fill-white fill-warning">
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path <path

View File

@@ -2,7 +2,12 @@
'status' => 'Restarting', 'status' => 'Restarting',
]) ])
<x-loading wire:loading.delay.longer /> <x-loading wire:loading.delay.longer />
<div class="flex items-center gap-2" wire:loading.remove.delay.longer> <div class="flex items-center " wire:loading.remove.delay.longer>
<div class="badge badge-warning badge-xs"></div> <div class="badge badge-warning badge-xs"></div>
<div class="text-xs font-medium tracking-wide text-warning">{{ Str::headline($status) }}</div> <div class="pl-2 pr-1 text-xs font-bold tracking-widerr text-warning">
{{ str($status)->before(':')->headline() }}
</div>
@if (!str($status)->startsWith('Proxy') && !str($status)->contains('('))
<div class="text-xs text-warning">({{ str($status)->after(':') }})</div>
@endif
</div> </div>

View File

@@ -2,7 +2,12 @@
'status' => 'Running', 'status' => 'Running',
]) ])
<x-loading wire:loading.delay.longer /> <x-loading wire:loading.delay.longer />
<div class="flex items-center gap-2 " wire:loading.remove.delay.longer> <div class="flex items-center" wire:loading.remove.delay.longer>
<div class="badge badge-success badge-xs"></div> <div class="badge badge-success badge-xs"></div>
<div class="text-xs font-medium tracking-wide text-success">{{ Str::headline($status) }}</div> <div class="pl-2 pr-1 text-xs font-bold tracking-wider text-success">
{{ str($status)->before(':')->headline() }}
</div>
@if (!str($status)->startsWith('Proxy') && !str($status)->contains('('))
<div class="text-xs text-success">({{ str($status)->after(':') }})</div>
@endif
</div> </div>

View File

@@ -7,7 +7,7 @@
@else @else
<x-status.stopped :status="$complexStatus" /> <x-status.stopped :status="$complexStatus" />
@endif @endif
@if (!str($complexStatus)->contains('exited')) @if (!str($complexStatus)->contains('exited') && $showRefreshButton)
<button title="Refresh Status" wire:click='check_status(true)' class="mx-1 hover:fill-white fill-warning"> <button title="Refresh Status" wire:click='check_status(true)' class="mx-1 hover:fill-white fill-warning">
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path <path

View File

@@ -2,7 +2,7 @@
'status' => 'Stopped', 'status' => 'Stopped',
]) ])
<x-loading wire:loading.delay.longer /> <x-loading wire:loading.delay.longer />
<div class="flex items-center gap-2 " wire:loading.remove.delay.longer> <div class="flex items-center" wire:loading.remove.delay.longer>
<div class="badge badge-error badge-xs"></div> <div class="badge badge-error badge-xs"></div>
<div class="text-xs font-medium tracking-wide text-error">{{ Str::headline($status) }}</div> <div class="pl-2 pr-1 text-xs font-bold tracking-wider text-error">{{ str($status)->before(':')->headline() }}</div>
</div> </div>

View File

@@ -1,15 +1,12 @@
@extends('layouts.base') @extends('layouts.base')
@section('body') @section('body')
@if (isSubscriptionActive() || isDev()) <div title="Send us feedback or get help!" class="fixed top-0 right-0 p-2 px-4 pt-4 mt-auto text-xs">
<div title="Send us feedback or get help!" class="fixed top-0 right-0 p-2 px-4 pt-4 mt-auto text-xs"> <button class="flex items-center justify-center gap-2" wire:click="help" onclick="help.showModal()">
<button class="flex items-center justify-center gap-2" wire:click="help" onclick="help.showModal()"> <svg class="icon" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<svg class="icon" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg"> <path fill="currentColor" d="M140 180a12 12 0 1 1-12-12a12 12 0 0 1 12 12M128 72c-22.06 0-40 16.15-40 36v4a8 8 0 0 0 16 0v-4c0-11 10.77-20 24-20s24 9 24 20s-10.77 20-24 20a8 8 0 0 0-8 8v8a8 8 0 0 0 16 0v-.72c18.24-3.35 32-17.9 32-35.28c0-19.85-17.94-36-40-36m104 56A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88"/>
<path fill="currentColor" </svg>
d="M144 180a16 16 0 1 1-16-16a16 16 0 0 1 16 16m92-52A108 108 0 1 1 128 20a108.12 108.12 0 0 1 108 108m-24 0a84 84 0 1 0-84 84a84.09 84.09 0 0 0 84-84m-84-64c-24.26 0-44 17.94-44 40v4a12 12 0 0 0 24 0v-4c0-8.82 9-16 20-16s20 7.18 20 16s-9 16-20 16a12 12 0 0 0-12 12v8a12 12 0 0 0 23.73 2.56C158.31 137.88 172 122.37 172 104c0-22.06-19.74-40-44-40" /> </button>
</svg> </div>
</button>
</div>
@endif
<main class="min-h-screen hero"> <main class="min-h-screen hero">
<div class="hero-content"> <div class="hero-content">
{{ $slot }} {{ $slot }}

View File

@@ -0,0 +1,17 @@
<div>
<h1>Admin Dashboard</h1>
<h3 class="pt-4">Who am I now?</h3>
{{ auth()->user()->name }}
<h3 class="pt-4">Users</h3>
<div class="flex flex-wrap gap-2">
<div class="w-96 box" wire:click="switchUser('0')">
Root
</div>
@foreach ($users as $user)
<div class="w-96 box" wire:click="switchUser('{{ $user->id }}')">
<p>{{ $user->name }}</p>
<p>{{ $user->email }}</p>
</div>
@endforeach
</div>
</div>

View File

@@ -121,18 +121,24 @@
There are already servers available for your Team. Do you want to use one of them? There are already servers available for your Team. Do you want to use one of them?
</x-slot:question> </x-slot:question>
<x-slot:actions> <x-slot:actions>
<x-forms.button class="justify-center w-64 box" wire:click="createNewServer">No (create one for me) <div class="flex flex-col gap-4">
</x-forms.button> <div>
<div> <x-forms.button class="justify-center w-64 box" wire:click="createNewServer">No (create one
<form wire:submit='selectExistingServer' class="flex flex-col w-full gap-4 lg:w-96"> for
<x-forms.select label="Existing servers" class="w-96" id='selectedExistingServer'> me)
@foreach ($servers as $server) </x-forms.button>
<option wire:key="{{ $loop->index }}" value="{{ $server->id }}"> </div>
{{ $server->name }}</option> <div>
@endforeach <form wire:submit='selectExistingServer' class="flex flex-col w-full gap-4 lg:w-96">
</x-forms.select> <x-forms.select label="Existing servers" class="w-96" id='selectedExistingServer'>
<x-forms.button type="submit">Use this Server</x-forms.button> @foreach ($servers as $server)
</form> <option wire:key="{{ $loop->index }}" value="{{ $server->id }}">
{{ $server->name }}</option>
@endforeach
</x-forms.select>
<x-forms.button type="submit">Use this Server</x-forms.button>
</form>
</div>
</div> </div>
@if (!$serverReachable) @if (!$serverReachable)
This server is not reachable with the following public key. This server is not reachable with the following public key.
@@ -216,7 +222,7 @@
helper="If you are using Cloudflare Tunnels, enable this. It will proxy all ssh requests to your server through Cloudflare.<br><span class='text-warning'>Coolify does not install/setup Cloudflare (cloudflared) on your server.</span>" helper="If you are using Cloudflare Tunnels, enable this. It will proxy all ssh requests to your server through Cloudflare.<br><span class='text-warning'>Coolify does not install/setup Cloudflare (cloudflared) on your server.</span>"
id="isCloudflareTunnel" label="Cloudflare Tunnel" /> id="isCloudflareTunnel" label="Cloudflare Tunnel" />
</div> </div>
<x-forms.button type="submit">Check Connection</x-forms.button> <x-forms.button type="submit">Continue</x-forms.button>
</form> </form>
</x-slot:actions> </x-slot:actions>
<x-slot:explanation> <x-slot:explanation>
@@ -227,14 +233,15 @@
@endif @endif
</div> </div>
<div> <div>
@if ($currentState === 'install-docker') @if ($currentState === 'validate-server')
<x-boarding-step title="Install Docker"> <x-boarding-step title="Validate & Configure Server">
<x-slot:question> <x-slot:question>
Could not find Docker Engine on your server. Do you want me to install it for you? I need to validate your server (connection, Docker Engine, etc) and configure if something is
missing for me. Are you okay with this?
</x-slot:question> </x-slot:question>
<x-slot:actions> <x-slot:actions>
<x-slide-over closeWithX fullScreen> <x-slide-over closeWithX fullScreen>
<x-slot:title>Configuring Server</x-slot:title> <x-slot:title>Validate & configure</x-slot:title>
<x-slot:content> <x-slot:content>
<livewire:server.validate-and-install :server="$this->createdServer" /> <livewire:server.validate-and-install :server="$this->createdServer" />
</x-slot:content> </x-slot:content>
@@ -254,7 +261,7 @@
</x-boarding-step> </x-boarding-step>
@endif @endif
</div> </div>
<div> {{-- <div>
@if ($currentState === 'select-proxy') @if ($currentState === 'select-proxy')
<x-boarding-step title="Select a Proxy"> <x-boarding-step title="Select a Proxy">
<x-slot:question> <x-slot:question>
@@ -281,7 +288,7 @@
</x-slot:explanation> </x-slot:explanation>
</x-boarding-step> </x-boarding-step>
@endif @endif
</div> </div> --}}
<div> <div>
@if ($currentState === 'create-project') @if ($currentState === 'create-project')
<x-boarding-step title="Project"> <x-boarding-step title="Project">

View File

@@ -29,7 +29,7 @@
@endif @endif
@foreach ($projects as $project) @foreach ($projects as $project)
<div class="gap-2 border border-transparent cursor-pointer box group"> <div class="gap-2 border border-transparent cursor-pointer box group">
@if (data_get($project, 'environments.0.name')) @if (data_get($project, 'environments')->count() === 1)
<a class="flex flex-col flex-1 mx-6 hover:no-underline" <a class="flex flex-col flex-1 mx-6 hover:no-underline"
href="{{ route('project.resource.index', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => data_get($project, 'environments.0.name', 'production')]) }}"> href="{{ route('project.resource.index', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => data_get($project, 'environments.0.name', 'production')]) }}">
<div class="font-bold text-white">{{ $project->name }}</div> <div class="font-bold text-white">{{ $project->name }}</div>
@@ -106,6 +106,7 @@
@if (count($deployments_per_server) > 0) @if (count($deployments_per_server) > 0)
<x-loading /> <x-loading />
@endif @endif
<x-forms.button wire:click='cleanup_queue'>Cleanup Queues</x-forms.button>
</div> </div>
<div wire:poll.1000ms="get_deployments" class="grid grid-cols-1"> <div wire:poll.1000ms="get_deployments" class="grid grid-cols-1">
@forelse ($deployments_per_server as $server_name => $deployments) @forelse ($deployments_per_server as $server_name => $deployments)

View File

@@ -15,7 +15,11 @@
@endif @endif
<x-forms.checkbox <x-forms.checkbox
helper="Your application will be available only on https if your domain starts with https://..." helper="Your application will be available only on https if your domain starts with https://..."
instantSave id="application.settings.is_force_https_enabled" label="Force Https" /> instantSave id="is_force_https_enabled" label="Force Https" />
<x-forms.checkbox
helper="The deployed container will have the same name ({{ $application->uuid }}). <span class='font-bold text-warning'>You will lose the rolling update feature!</span>"
instantSave id="application.settings.is_consistent_container_name_enabled"
label="Consistent Container Names" />
<h4>Logs</h4> <h4>Logs</h4>
@if (!$application->settings->is_raw_compose_deployment_enabled) @if (!$application->settings->is_raw_compose_deployment_enabled)
<x-forms.checkbox helper="Drain logs to your configured log drain endpoint in your Server settings." <x-forms.checkbox helper="Drain logs to your configured log drain endpoint in your Server settings."

View File

@@ -27,8 +27,8 @@
<a :class="activeTab === 'source' && 'text-white'" <a :class="activeTab === 'source' && 'text-white'"
@click.prevent="activeTab = 'source'; window.location.hash = 'source'" href="#">Source</a> @click.prevent="activeTab = 'source'; window.location.hash = 'source'" href="#">Source</a>
@endif @endif
<a :class="activeTab === 'server' && 'text-white'" <a :class="activeTab === 'servers' && 'text-white'"
@click.prevent="activeTab = 'server'; window.location.hash = 'server'" href="#">Server @click.prevent="activeTab = 'servers'; window.location.hash = 'servers'" href="#">Servers
</a> </a>
<a :class="activeTab === 'scheduled-tasks' && 'text-white'" <a :class="activeTab === 'scheduled-tasks' && 'text-white'"
@@ -88,7 +88,7 @@
<livewire:project.application.source :application="$application" /> <livewire:project.application.source :application="$application" />
</div> </div>
@endif @endif
<div x-cloak x-show="activeTab === 'server'"> <div x-cloak x-show="activeTab === 'servers'">
<livewire:project.shared.destination :resource="$application" :servers="$servers" /> <livewire:project.shared.destination :resource="$application" :servers="$servers" />
</div> </div>
<div x-cloak x-show="activeTab === 'storages'"> <div x-cloak x-show="activeTab === 'storages'">

View File

@@ -77,6 +77,11 @@
Manual Manual
</div> </div>
@endif @endif
@if (data_get($deployment, 'server_name'))
<div class="flex gap-1">
Server: {{ data_get($deployment, 'server_name') }}
</div>
@endif
</div> </div>
<div class="flex flex-col" x-data="elapsedTime('{{ $deployment->deployment_uuid }}', '{{ $deployment->status }}', '{{ $deployment->created_at }}', '{{ $deployment->updated_at }}')"> <div class="flex flex-col" x-data="elapsedTime('{{ $deployment->deployment_uuid }}', '{{ $deployment->status }}', '{{ $deployment->created_at }}', '{{ $deployment->updated_at }}')">

View File

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

View File

@@ -27,7 +27,7 @@
@if (!$application->destination->server->isSwarm()) @if (!$application->destination->server->isSwarm())
<x-applications.advanced :application="$application" /> <x-applications.advanced :application="$application" />
@endif @endif
@if ($application->status !== 'exited') @if (!str($application->status)->startsWith('exited'))
@if (!$application->destination->server->isSwarm()) @if (!$application->destination->server->isSwarm())
<button title="With rolling update if possible" wire:click='deploy' <button title="With rolling update if possible" wire:click='deploy'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400"> class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">

View File

@@ -1,5 +1,9 @@
<div> <div>
<livewire:project.application.preview.form :application="$application" /> <livewire:project.application.preview.form :application="$application" />
@if (count($application->additional_servers) > 0)
<div class="pb-4">Previews will be deployed on <span
class="text-warning">{{ $application->destination->server->name }}</span>.</div>
@endif
<div> <div>
@if ($application->is_github_based()) @if ($application->is_github_based())
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@@ -51,7 +55,7 @@
</div> </div>
@if ($application->previews->count() > 0) @if ($application->previews->count() > 0)
<div class="pb-4">Previews</div> <div class="pb-4">Previews</div>
<div class="flex gap-6 "> <div class="flex flex-wrap gap-6">
@foreach ($application->previews as $preview) @foreach ($application->previews as $preview)
<div class="flex flex-col p-4 bg-coolgray-200"> <div class="flex flex-col p-4 bg-coolgray-200">
<div class="flex gap-2">PR #{{ data_get($preview, 'pull_request_id') }} | <div class="flex gap-2">PR #{{ data_get($preview, 'pull_request_id') }} |

View File

@@ -21,10 +21,10 @@
@click.prevent="activeTab = 'environment-variables'; window.location.hash = 'environment-variables'" @click.prevent="activeTab = 'environment-variables'; window.location.hash = 'environment-variables'"
href="#">Environment href="#">Environment
Variables</a> Variables</a>
<a :class="activeTab === 'server' && 'text-white'" <a :class="activeTab === 'servers' && 'text-white'"
@click.prevent="activeTab = 'server'; @click.prevent="activeTab = 'servers';
window.location.hash = 'server'" window.location.hash = 'servers'"
href="#">Server href="#">Servers
</a> </a>
<a :class="activeTab === 'storages' && 'text-white'" <a :class="activeTab === 'storages' && 'text-white'"
@click.prevent="activeTab = 'storages'; @click.prevent="activeTab = 'storages';
@@ -74,7 +74,7 @@
<div x-cloak x-show="activeTab === 'environment-variables'"> <div x-cloak x-show="activeTab === 'environment-variables'">
<livewire:project.shared.environment-variable.all :resource="$database" /> <livewire:project.shared.environment-variable.all :resource="$database" />
</div> </div>
<div x-cloak x-show="activeTab === 'server'"> <div x-cloak x-show="activeTab === 'servers'">
<livewire:project.shared.destination :resource="$database" /> <livewire:project.shared.destination :resource="$database" />
</div> </div>
<div x-cloak x-show="activeTab === 'storages'"> <div x-cloak x-show="activeTab === 'storages'">

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