Compare commits

...

143 Commits

Author SHA1 Message Date
Andras Bacsai
53975fcf61 Merge pull request #1516 from coollabsio/next
v4.0.0-beta.152
2023-12-04 11:26:17 +01:00
Andras Bacsai
76296c1f19 fix: prevent autorefresh of proxy status 2023-12-04 11:25:24 +01:00
Andras Bacsai
c25baf69e1 fix: workdir issue for basedir
fix: remove / mount on helpers image
2023-12-04 11:20:50 +01:00
Andras Bacsai
f952512615 fix: add cf tunnel to boarding server view 2023-12-04 09:29:55 +01:00
Andras Bacsai
c6557eada8 service: meilisearch 2023-12-03 12:16:33 +01:00
Andras Bacsai
2c2d74c0d6 Update release version to 4.0.0-beta.152 2023-12-01 22:16:48 +01:00
Andras Bacsai
028a2eb275 Fix Docker compose build command and remove debug statements 2023-12-01 22:16:27 +01:00
Andras Bacsai
ce7fad5bef Merge pull request #1511 from coollabsio/next
fix: use official install script with rancher (one will work for sure)
2023-12-01 14:02:30 +01:00
Andras Bacsai
cd7852e4f9 fix: use official install script with rancher (one will work for sure) 2023-12-01 14:02:11 +01:00
Andras Bacsai
7b022a2482 Merge pull request #1510 from coollabsio/next
v4.0.0-beta.151
2023-12-01 13:03:42 +01:00
Andras Bacsai
12d9b6538b Fix environment variable parsing in Docker Compose file 2023-12-01 12:34:23 +01:00
Andras Bacsai
335788c2d6 fix: default value do not overwrite existing env value 2023-12-01 12:14:23 +01:00
Andras Bacsai
2352e4a71d Fix directory creation inApplicationDeploymentJob.php 2023-12-01 12:13:55 +01:00
Andras Bacsai
dc03179bd1 feat: auto-restart tcp proxies for databases 2023-12-01 11:37:00 +01:00
Andras Bacsai
cc72f416e8 feat: custom log drain endpoints 2023-12-01 11:13:58 +01:00
Andras Bacsai
3b67d0a8de feat: save timestamp configuration for logs 2023-12-01 10:34:30 +01:00
Andras Bacsai
0135ba7e89 Delete docker-compose.prod.standalone.yml 2023-11-30 13:38:52 +01:00
Andras Bacsai
a28a28cd23 Add docker-compose.prod.standalone.yml
configuration file
2023-11-30 13:17:43 +01:00
Andras Bacsai
b52680a2d8 Fix dispatch_sync issue in ContainerStatusJob 2023-11-30 12:55:31 +01:00
Andras Bacsai
0670e6c1d6 fix: server view for link() 2023-11-30 12:21:53 +01:00
Andras Bacsai
c3882b75c1 Update release version to 4.0.0-beta.151 2023-11-30 12:21:42 +01:00
Andras Bacsai
64b6f86a36 Update PostgreSQL image version to 16-alpine for services 2023-11-30 12:21:21 +01:00
Andras Bacsai
b9efc22253 Merge pull request #1504 from coollabsio/next
v4.0.0-beta.150 - quick fix
2023-11-29 18:44:25 +01:00
Andras Bacsai
e3d9eb0154 Add hidden flag to docker compose command 2023-11-29 18:43:02 +01:00
Andras Bacsai
66f3967479 Merge pull request #1503 from coollabsio/next
v4.0.0-beta.150
2023-11-29 18:41:41 +01:00
Andras Bacsai
c54439e84c fix: dockercompose save ./ volumes under /data/coolify 2023-11-29 18:40:41 +01:00
Andras Bacsai
db4a4c74fc Merge pull request #1502 from coollabsio/next
v4.0.0-beta.149
2023-11-29 17:05:28 +01:00
Andras Bacsai
5c7ef80219 Fix container retrieval in CheckLogDrainContainerJob and ContainerStatusJob 2023-11-29 17:03:04 +01:00
Andras Bacsai
243d1c06fc cloud: disable trial 2023-11-29 16:34:31 +01:00
Andras Bacsai
ef25f7d800 Update Sentry DSN 2023-11-29 16:21:03 +01:00
Andras Bacsai
45640ffdb1 Update version numbers to 4.0.0-beta.149 2023-11-29 16:19:40 +01:00
Andras Bacsai
378291b209 Merge pull request #1501 from coollabsio/next
Commented out cleanup_ssh() function
2023-11-29 15:23:21 +01:00
Andras Bacsai
3583e552f1 Commented out cleanup_ssh() function 2023-11-29 15:23:03 +01:00
Andras Bacsai
d7dfeaf988 Merge pull request #1496 from coollabsio/next
v4.0.0-beta.148
2023-11-29 15:17:39 +01:00
Andras Bacsai
7fe5eca661 Add precheck for containers 2023-11-29 15:13:03 +01:00
Andras Bacsai
0dff57e69f Add cleanup option to app:init command 2023-11-29 15:03:21 +01:00
Andras Bacsai
f4803ad58b wip: swarm
fix: gitcompose deployments
2023-11-29 14:59:06 +01:00
Andras Bacsai
2d7bbbe300 wip: swarm 2023-11-29 10:06:52 +01:00
Andras Bacsai
928b68043b wip: swarm 2023-11-28 20:49:38 +01:00
Andras Bacsai
b21add0210 Update Swarm cluster label to Swarm Manager 2023-11-28 20:09:00 +01:00
Andras Bacsai
c41ffd6bfb wip: swarm 2023-11-28 18:42:09 +01:00
Andras Bacsai
b4874c7df3 wip: swarm 2023-11-28 18:31:04 +01:00
Andras Bacsai
c505a6ce9c wip 2023-11-28 15:49:24 +01:00
Andras Bacsai
706e4b13ee fix: sentry issue 2023-11-28 14:27:38 +01:00
Andras Bacsai
4af471ee31 fix: no container servers 2023-11-28 14:26:35 +01:00
Andras Bacsai
87062e4e22 Refactor application deployment job 2023-11-28 14:23:59 +01:00
Andras Bacsai
500ba0fab8 fix: do not remove deployment in case compose based failed 2023-11-28 14:08:42 +01:00
Andras Bacsai
1c72c127d5 Remove unused imports and fix import statement 2023-11-28 14:05:55 +01:00
Andras Bacsai
69bb4ae5ee Update release version to 4.0.0-beta.148 2023-11-28 13:40:33 +01:00
Andras Bacsai
5f8b8bd730 Merge pull request #1480 from coollabsio/next
v4.0.0-beta.147
2023-11-28 13:39:34 +01:00
Andras Bacsai
44f6d93639 Update installation script to include curl and wget 2023-11-28 13:28:15 +01:00
Andras Bacsai
e35b8a0f96 Add Stringable interface to validateOS method 2023-11-28 13:21:32 +01:00
Andras Bacsai
b26e23e7c3 Fix validateOS() return type 2023-11-28 13:17:59 +01:00
Andras Bacsai
e6f7e32037 Add SUPPORTED_OS constant based on /etc/os-release 2023-11-28 13:12:42 +01:00
Andras Bacsai
1c386db41d Update Docker installation command and add support for SLES 2023-11-28 13:12:25 +01:00
Andras Bacsai
085b655d9f Update version to 1.1.0 and add support for Redhat and Sles based operating systems 2023-11-28 13:02:12 +01:00
Andras Bacsai
2788fcf4e1 Add Docker Compose based applications and preview deployments to proxy on restart 2023-11-28 12:48:55 +01:00
Andras Bacsai
d058e04213 Add fqdn attribute to InstanceSettings model 2023-11-28 12:11:03 +01:00
Andras Bacsai
066f171163 Add Docker Compose file for Formbricks service 2023-11-28 12:05:14 +01:00
Andras Bacsai
2001be07d0 refactor: env variable generator 2023-11-28 12:05:04 +01:00
Andras Bacsai
39552cc42f fix: double default password length 2023-11-28 12:04:21 +01:00
Andras Bacsai
7f5d7e0eb0 Refactor application submit method to handle dockercompose build pack 2023-11-28 11:10:48 +01:00
Andras Bacsai
0eda49b104 fix: pull request build variables 2023-11-28 11:10:42 +01:00
Andras Bacsai
636995d0e4 Refactor server delete view 2023-11-28 10:55:24 +01:00
Andras Bacsai
4c0623f022 Refactor server delete view to display defined resources as links 2023-11-28 10:54:46 +01:00
Andras Bacsai
3e2e1080f5 nothing to see here 2023-11-28 10:46:00 +01:00
Andras Bacsai
3f866a07d8 Fix docker compose PR location default value 2023-11-28 10:11:53 +01:00
Andras Bacsai
23571ae104 wip 2023-11-27 15:50:22 +01:00
Andras Bacsai
c1710c8f7b moar fixes 2023-11-27 15:25:15 +01:00
Andras Bacsai
d4d2cc71a0 fix: lots of regarding git + docker compose deployments 2023-11-27 14:28:21 +01:00
Andras Bacsai
8d86d53292 fix: new logging for deployment jobs
fix: git based docker compose files
2023-11-27 11:54:55 +01:00
Andras Bacsai
fae97e4dee Fix network connection issues in Server and Service models 2023-11-27 09:58:31 +01:00
Andras Bacsai
8d0c3abf2e Refactor server delete view to display defined
resources
2023-11-27 09:42:23 +01:00
Andras Bacsai
d396f649df fix: show defined resources in server tab, so you will know what you need to delete before you can delete the server. 2023-11-27 09:39:43 +01:00
Andras Bacsai
ec21155c9e Update rules for field validation in StackForm.php 2023-11-24 21:38:39 +01:00
Andras Bacsai
58111f53b9 test wire:ignore 2023-11-24 21:35:01 +01:00
Andras Bacsai
2cbe1e8489 Add SMTP mail transport option to Ghost compose
file
2023-11-24 21:23:48 +01:00
Andras Bacsai
10e5a58b9e Add extra fields for MinIO, Weblate, and Ghost services 2023-11-24 21:04:15 +01:00
Andras Bacsai
6f886e8b6f Update Ghost configuration with mail options 2023-11-24 21:03:59 +01:00
Andras Bacsai
f96a91eb31 wip: compose based apps 2023-11-24 15:48:23 +01:00
Andras Bacsai
65a1961722 Add environment variables for Horizon balance 2023-11-24 10:12:37 +01:00
Andras Bacsai
c5a932ab88 Add environment variables for GitHub
authentication and email configuration
2023-11-24 08:38:49 +01:00
Andras Bacsai
d1e10dacc0 wip 2023-11-23 21:02:30 +01:00
Andras Bacsai
96327af838 Update log-drains.blade.php and add
trigger-with-external-database.yaml and
service-templates.json
2023-11-23 12:44:08 +01:00
Andras Bacsai
1cb6d594d0 Fix service loading issue in project select page 2023-11-23 11:49:49 +01:00
Andras Bacsai
16261fc36e Remove unnecessary code and update services list
loading
2023-11-23 11:40:29 +01:00
Andras Bacsai
cff694b0c4 Update Weblate configuration 2023-11-23 11:35:19 +01:00
Andras Bacsai
97fd56b9e4 Update number of servers in pricing plans 2023-11-23 10:57:11 +01:00
Andras Bacsai
72cfa3e7b0 Update server limits using environment variables 2023-11-23 10:51:57 +01:00
Andras Bacsai
3cf41e1e23 Update server basic value 2023-11-23 10:47:25 +01:00
Andras Bacsai
2a7a63a672 Add trigger.dev service 2023-11-23 09:05:22 +01:00
Andras Bacsai
7fb9e672cf Fix server execution method parameter name 2023-11-22 20:56:25 +01:00
Andras Bacsai
9012f6b953 Fix GitHub App retrieval in webhooks.php 2023-11-22 16:40:49 +01:00
Andras Bacsai
407eba8b76 Fix DockerCleanupJob exception message 2023-11-22 16:39:16 +01:00
Andras Bacsai
68f6ab5796 wip 2023-11-22 15:18:49 +01:00
Andras Bacsai
3dd36a2271 Fix container status handling and notifications 2023-11-22 15:18:37 +01:00
Andras Bacsai
7f69eb3c2e Merge pull request #1479 from coollabsio/next
v4.0.0-beta.146 - quick fix before release
2023-11-22 14:27:04 +01:00
Andras Bacsai
6ccbf911b2 Fix condition for pushing to Docker registry 2023-11-22 14:25:55 +01:00
Andras Bacsai
5c77cec68f Merge pull request #1478 from coollabsio/next
v4.0.0-beta.146
2023-11-22 14:22:10 +01:00
Andras Bacsai
25a0489f7f Fix log drain issue in advanced and service application 2023-11-22 14:21:03 +01:00
Andras Bacsai
5e27b88bef Add new console commands for root email change, root password reset, and service deletion 2023-11-22 13:21:25 +01:00
Andras Bacsai
ec98afe707 Merge pull request #1474 from coollabsio/next
v4.0.0-beta.145
2023-11-22 08:45:00 +01:00
Andras Bacsai
ce26127705 wip: new deployment jobs 2023-11-21 22:17:35 +01:00
Andras Bacsai
ef7fc1b260 Refactor code and update destination component 2023-11-21 15:31:46 +01:00
Andras Bacsai
f58e6766e1 Update Docker Engine version check 2023-11-21 13:06:05 +01:00
Andras Bacsai
4a21102983 fix: server adding process 2023-11-21 12:07:06 +01:00
Andras Bacsai
e78b6758d8 feat: add docker engine support install script to rhel based systems 2023-11-21 11:39:19 +01:00
Andras Bacsai
16eb7f4fb4 Add tracing option to Sentry configuration 2023-11-21 09:01:52 +01:00
Andras Bacsai
4974ce6eda Update release version to 4.0.0-beta.145 2023-11-21 08:41:43 +01:00
Andras Bacsai
6cdba17aca Update token retrieval in reset-password.blade.php 2023-11-20 15:16:23 +01:00
Andras Bacsai
30f8e8f232 fix: handle different label formats in services 2023-11-20 15:01:35 +01:00
Andras Bacsai
608f0b7840 Refactor Docker image name generation and push to
registry
2023-11-20 14:23:11 +01:00
Andras Bacsai
d0366c4054 Update Docker Registry link in general.blade.php 2023-11-20 13:58:31 +01:00
Andras Bacsai
f88e3c5b29 feat: push locally built image to docker registry
ui: fixes here and there
2023-11-20 13:49:10 +01:00
Andras Bacsai
e33fec0e1a Refactor checkbox component and update GPU
settings helper links
2023-11-20 11:37:09 +01:00
Andras Bacsai
912b0a263e feat: gpu enabled containers
feat: move advanced settings to different view
2023-11-20 11:35:31 +01:00
Andras Bacsai
8f963adbd4 fix: only report nonruntime errors 2023-11-20 10:32:06 +01:00
Andras Bacsai
8f2c24d7e9 fix: reset password 2023-11-18 17:50:44 +01:00
Andras Bacsai
9f3dbc3cbb Merge pull request #1469 from coollabsio/next
v4.0.0-beta.144
2023-11-17 21:28:18 +01:00
Andras Bacsai
8a9ee84925 Fix log drain container notification bug 2023-11-17 21:24:22 +01:00
Andras Bacsai
689480003a feat: log drainer container check 2023-11-17 21:16:25 +01:00
Andras Bacsai
3b20eee909 feat: enable/disable log drain by service 2023-11-17 20:08:21 +01:00
Andras Bacsai
e8cadc176b Merge pull request #1468 from coollabsio/next
v4.0.0-beta.143
2023-11-17 15:21:29 +01:00
Andras Bacsai
b0c96e64c9 Fix server unreachable notification count 2023-11-17 15:18:08 +01:00
Andras Bacsai
9ce3b43e09 Add Team model and merge servers with own servers 2023-11-17 15:11:29 +01:00
Andras Bacsai
4c2b3df861 Update server runtime and comments 2023-11-17 14:56:39 +01:00
Andras Bacsai
467471f54a Fix server readiness check in ContainerStatusJob and ServerStatusJob 2023-11-17 14:46:04 +01:00
Andras Bacsai
60171093c5 Update version to 4.0.0-beta.143 2023-11-17 14:43:57 +01:00
Andras Bacsai
38f2a2dac7 Merge pull request #1467 from coollabsio/next
v4.0.0-beta.142
2023-11-17 14:32:32 +01:00
Andras Bacsai
307ee52ac0 wtf 2023-11-17 14:29:37 +01:00
Andras Bacsai
b66c9835b7 Fix server status check and add new job 2023-11-17 14:22:05 +01:00
Andras Bacsai
d38d50dca2 Fix server readiness check and update version
number
2023-11-17 14:14:13 +01:00
Andras Bacsai
40023be4ea Merge pull request #1466 from coollabsio/next
Quick fix version
2023-11-17 14:02:00 +01:00
Andras Bacsai
48d7c6e76f Fix config version key 2023-11-17 13:59:45 +01:00
Andras Bacsai
debacfe2f7 Merge pull request #1465 from coollabsio/next
v4.0.0-beta.141
2023-11-17 13:54:20 +01:00
Andras Bacsai
d430813230 Update versions and add server readiness check 2023-11-17 13:53:56 +01:00
Andras Bacsai
e30c37b041 Merge pull request #1464 from coollabsio/next
v4.0.0-beta.140
2023-11-17 13:25:03 +01:00
Andras Bacsai
8c73068cc7 Refactor server filtering logic in Kernel.php 2023-11-17 13:11:46 +01:00
Andras Bacsai
2c4e69ad50 Fix server readiness check in ContainerStatusJob
and ServerStatusJob
2023-11-17 13:04:51 +01:00
Andras Bacsai
5ae08d009e Add skipServer() method to Server model 2023-11-17 12:47:15 +01:00
Andras Bacsai
673b944647 Fix IP address validation in server forms 2023-11-17 12:38:47 +01:00
Andras Bacsai
16281248ac Refactor Dockerfile deployment logic and server
validation
2023-11-17 12:22:45 +01:00
Andras Bacsai
8670b41671 fix: do not allow to enter local ip addresses 2023-11-17 11:56:14 +01:00
157 changed files with 5395 additions and 1971 deletions

View File

@@ -11,19 +11,34 @@ class StopApplication
public function handle(Application $application)
{
$server = $application->destination->server;
$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
);
if ($server->isSwarm()) {
instant_remote_process(["docker stack rm {$application->uuid}" ], $server);
} else {
$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
);
}
}
// 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);
}
}
// TODO: make notification for application
// $application->environment->project->team->notify(new StatusChanged($application));
}
}
}

View File

@@ -23,7 +23,7 @@ class StartMariadb
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
"echo '####### Starting {$database->name}.'",
"echo 'Starting {$database->name}.'",
"mkdir -p $this->configuration_dir",
];
@@ -69,7 +69,7 @@ class StartMariadb
]
]
];
if ($this->database->destination->server->isDrainLogActivated()) {
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
@@ -104,7 +104,7 @@ class StartMariadb
$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 up -d";
$this->commands[] = "echo '####### {$database->name} started.'";
$this->commands[] = "echo '{$database->name} started.'";
return remote_process($this->commands, $database->destination->server);
}

View File

@@ -25,7 +25,7 @@ class StartMongodb
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
"echo '####### Starting {$database->name}.'",
"echo 'Starting {$database->name}.'",
"mkdir -p $this->configuration_dir",
];
@@ -76,7 +76,7 @@ class StartMongodb
]
]
];
if ($this->database->destination->server->isDrainLogActivated()) {
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
@@ -120,7 +120,7 @@ class StartMongodb
$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 up -d";
$this->commands[] = "echo '####### {$database->name} started.'";
$this->commands[] = "echo '{$database->name} started.'";
return remote_process($this->commands, $database->destination->server);
}

View File

@@ -23,7 +23,7 @@ class StartMysql
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
"echo '####### Starting {$database->name}.'",
"echo 'Starting {$database->name}.'",
"mkdir -p $this->configuration_dir",
];
@@ -69,7 +69,7 @@ class StartMysql
]
]
];
if ($this->database->destination->server->isDrainLogActivated()) {
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
@@ -104,7 +104,7 @@ class StartMysql
$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 up -d";
$this->commands[] = "echo '####### {$database->name} started.'";
$this->commands[] = "echo '{$database->name} started.'";
return remote_process($this->commands, $database->destination->server);
}

View File

@@ -23,7 +23,7 @@ class StartPostgresql
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
"echo '####### Starting {$database->name}.'",
"echo 'Starting {$database->name}.'",
"mkdir -p $this->configuration_dir",
"mkdir -p $this->configuration_dir/docker-entrypoint-initdb.d/"
];
@@ -79,7 +79,8 @@ class StartPostgresql
]
]
];
if ($this->database->destination->server->isDrainLogActivated()) {
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
ray('Log Drain Enabled');
$docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
@@ -129,7 +130,7 @@ class StartPostgresql
$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 up -d";
$this->commands[] = "echo '####### {$database->name} started.'";
$this->commands[] = "echo '{$database->name} started.'";
return remote_process($this->commands, $database->destination->server);
}

View File

@@ -26,7 +26,7 @@ class StartRedis
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
"echo '####### Starting {$database->name}.'",
"echo 'Starting {$database->name}.'",
"mkdir -p $this->configuration_dir",
];
@@ -78,7 +78,7 @@ class StartRedis
]
]
];
if ($this->database->destination->server->isDrainLogActivated()) {
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
@@ -114,7 +114,7 @@ class StartRedis
$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 up -d";
$this->commands[] = "echo '####### {$database->name} started.'";
$this->commands[] = "echo '{$database->name} started.'";
return remote_process($this->commands, $database->destination->server);
}
@@ -166,6 +166,5 @@ class StartRedis
$content = $this->database->redis_conf;
$content_base64 = base64_encode($content);
$this->commands[] = "echo '{$content_base64}' | base64 -d > $this->configuration_dir/{$filename}";
}
}

View File

@@ -17,35 +17,42 @@ class CheckProxy
return false;
}
}
$status = getContainerStatus($server, 'coolify-proxy');
if ($status === 'running') {
$server->proxy->set('status', 'running');
if ($server->isSwarm()) {
$status = getContainerStatus($server, 'coolify-proxy_traefik');
$server->proxy->set('status', $status);
$server->save();
return false;
}
$ip = $server->ip;
if ($server->id === 0) {
$ip = 'host.docker.internal';
}
} else {
$status = getContainerStatus($server, 'coolify-proxy');
if ($status === 'running') {
$server->proxy->set('status', 'running');
$server->save();
return false;
}
$ip = $server->ip;
if ($server->id === 0) {
$ip = 'host.docker.internal';
}
$connection80 = @fsockopen($ip, '80');
$connection443 = @fsockopen($ip, '443');
$port80 = is_resource($connection80) && fclose($connection80);
$port443 = is_resource($connection443) && fclose($connection443);
if ($port80) {
if ($fromUI) {
throw new \Exception("Port 80 is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a> <br> Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>");
} else {
return false;
$connection80 = @fsockopen($ip, '80');
$connection443 = @fsockopen($ip, '443');
$port80 = is_resource($connection80) && fclose($connection80);
$port443 = is_resource($connection443) && fclose($connection443);
if ($port80) {
if ($fromUI) {
throw new \Exception("Port 80 is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a> <br> Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>");
} else {
return false;
}
}
}
if ($port443) {
if ($fromUI) {
throw new \Exception("Port 443 is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a> <br> Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>");
} else {
return false;
if ($port443) {
if ($fromUI) {
throw new \Exception("Port 443 is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a> <br> Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>");
} else {
return false;
}
}
return true;
}
return true;
}
}

View File

@@ -13,6 +13,7 @@ class StartProxy
public function handle(Server $server, bool $async = true): string|Activity
{
try {
$proxyType = $server->proxyType();
$commands = collect([]);
$proxy_path = get_proxy_path();
@@ -24,18 +25,29 @@ class StartProxy
$docker_compose_yml_base64 = base64_encode($configuration);
$server->proxy->last_applied_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value;
$server->save();
$commands = $commands->merge([
"mkdir -p $proxy_path && cd $proxy_path",
"echo 'Creating required Docker Compose file.'",
"echo 'Pulling docker image.'",
'docker compose pull',
"echo 'Stopping existing coolify-proxy.'",
"docker compose down -v --remove-orphans > /dev/null 2>&1",
"echo 'Starting coolify-proxy.'",
'docker compose up -d --remove-orphans',
"echo 'Proxy started successfully.'"
]);
$commands = $commands->merge(connectProxyToNetworks($server));
if ($server->isSwarm()) {
$commands = $commands->merge([
"mkdir -p $proxy_path && cd $proxy_path",
"echo 'Creating required Docker Compose file.'",
"echo 'Starting coolify-proxy.'",
"cd $proxy_path && docker stack deploy -c docker-compose.yml coolify-proxy",
"echo 'Proxy started successfully.'"
]);
} else {
$commands = $commands->merge([
"mkdir -p $proxy_path && cd $proxy_path",
"echo 'Creating required Docker Compose file.'",
"echo 'Pulling docker image.'",
'docker compose pull',
"echo 'Stopping existing coolify-proxy.'",
"docker compose down -v --remove-orphans > /dev/null 2>&1",
"echo 'Starting coolify-proxy.'",
'docker compose up -d --remove-orphans',
"echo 'Proxy started successfully.'"
]);
$commands = $commands->merge(connectProxyToNetworks($server));
}
if ($async) {
$activity = remote_process($commands, $server);
return $activity;
@@ -46,11 +58,9 @@ class StartProxy
$server->save();
return 'OK';
}
} catch(\Throwable $e) {
} catch (\Throwable $e) {
ray($e);
throw $e;
}
}
}

View File

@@ -11,6 +11,11 @@ class InstallDocker
use AsAction;
public function handle(Server $server)
{
$supported_os_type = $server->validateOS();
if (!$supported_os_type) {
throw new \Exception('Server OS type is not supported for automated installation. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://coolify.io/docs/servers#install-docker-engine-manually">documentation</a>.');
}
ray('Installing Docker on server: ' . $server->name . ' (' . $server->ip . ')' . ' with OS type: ' . $supported_os_type);
$dockerVersion = '24.0';
$config = base64_encode('{
"log-driver": "json-file",
@@ -27,36 +32,65 @@ class InstallDocker
'server_id' => $server->id,
]);
}
$command = collect([]);
if (isDev() && $server->id === 0) {
$command = [
"echo '####### Installing Prerequisites...'",
$command = $command->merge([
"echo 'Installing Prerequisites...'",
"sleep 1",
"echo '####### Installing/updating Docker Engine...'",
"echo '####### Configuring Docker Engine (merging existing configuration with the required)...'",
"echo 'Installing Docker Engine...'",
"echo 'Configuring Docker Engine (merging existing configuration with the required)...'",
"sleep 4",
"echo '####### Restarting Docker Engine...'",
"echo 'Restarting Docker Engine...'",
"ls -l /tmp"
];
]);
} else {
$command = [
"echo '####### Installing Prerequisites...'",
"command -v jq >/dev/null || apt-get update",
"command -v jq >/dev/null || apt install -y jq",
"echo '####### Installing/updating Docker Engine...'",
"curl https://releases.rancher.com/install-docker/{$dockerVersion}.sh | sh",
"echo '####### Configuring Docker Engine (merging existing configuration with the required)...'",
if ($supported_os_type->contains('debian')) {
$command = $command->merge([
"echo 'Installing Prerequisites...'",
"command -v jq >/dev/null || apt-get update -y",
"command -v jq >/dev/null || apt install -y curl wget git jq",
]);
} else if ($supported_os_type->contains('rhel')) {
$command = $command->merge([
"echo 'Installing Prerequisites...'",
"command -v jq >/dev/null || dnf install -y curl wget git jq",
]);
} else if ($supported_os_type->contains('sles')) {
$command = $command->merge([
"echo 'Installing Prerequisites...'",
"command -v jq >/dev/null || zypper update -y",
"command -v jq >/dev/null || zypper install -y curl wget git jq",
]);
} else {
throw new \Exception('Unsupported OS');
}
$command = $command->merge([
"echo 'Installing Docker Engine...'",
"curl https://releases.rancher.com/install-docker/{$dockerVersion}.sh | sh || curl https://get.docker.com | sh -s -- --version {$dockerVersion}",
"echo 'Configuring Docker Engine (merging existing configuration with the required)...'",
"test -s /etc/docker/daemon.json && cp /etc/docker/daemon.json \"/etc/docker/daemon.json.original-`date +\"%Y%m%d-%H%M%S\"`\" || echo '{$config}' | base64 -d > /etc/docker/daemon.json",
"echo '{$config}' | base64 -d > /etc/docker/daemon.json.coolify",
"cat <<< $(jq . /etc/docker/daemon.json.coolify) > /etc/docker/daemon.json.coolify",
"cat <<< $(jq -s '.[0] * .[1]' /etc/docker/daemon.json /etc/docker/daemon.json.coolify) > /etc/docker/daemon.json",
"echo '####### Restarting Docker Engine...'",
"echo 'Restarting Docker Engine...'",
"systemctl enable docker >/dev/null 2>&1 || true",
"systemctl restart docker",
"echo '####### Creating default Docker network (coolify)...'",
"docker network create --attachable coolify >/dev/null 2>&1 || true",
"echo '####### Done!'"
];
]);
if ($server->isSwarm()) {
$command = $command->merge([
"docker network create --attachable --driver overlay coolify-overlay >/dev/null 2>&1 || true",
]);
} else {
$command = $command->merge([
"docker network create --attachable coolify >/dev/null 2>&1 || true",
]);
$command = $command->merge([
"echo 'Done!'",
]);
}
return remote_process($command, $server);
}
return remote_process($command, $server);
}
}

View File

@@ -8,8 +8,19 @@ use App\Models\Server;
class InstallLogDrain
{
use AsAction;
public function handle(Server $server, string $type)
public function handle(Server $server)
{
if ($server->settings->is_logdrain_newrelic_enabled) {
$type = 'newrelic';
} else if ($server->settings->is_logdrain_highlight_enabled) {
$type = 'highlight';
} else if ($server->settings->is_logdrain_axiom_enabled) {
$type = 'axiom';
} else if ($server->settings->is_logdrain_custom_enabled) {
$type = 'custom';
} else {
$type = 'none';
}
try {
if ($type === 'none') {
$command = [
@@ -105,15 +116,23 @@ class InstallLogDrain
json_date_format iso8601
tls On
");
} else if ($type === 'custom') {
if (!$server->settings->is_logdrain_custom_enabled) {
throw new \Exception('Custom log drain is not enabled.');
}
$config = base64_encode($server->settings->logdrain_custom_config);
$parsers = base64_encode($server->settings->logdrain_custom_config_parser);
} else {
throw new \Exception('Unknown log drain type.');
}
$parsers = base64_encode("
if ($type !== 'custom') {
$parsers = base64_encode("
[PARSER]
Name empty_line_skipper
Format regex
Regex /^(?!\s*$).+/
Name empty_line_skipper
Format regex
Regex /^(?!\s*$).+/
");
}
$compose = base64_encode("
services:
coolify-log-drain:
@@ -127,6 +146,7 @@ services:
- ./parsers.conf:/parsers.conf
ports:
- 127.0.0.1:24224:24224
restart: unless-stopped
");
$readme = base64_encode('# New Relic Log Drain
This log drain is based on [Fluent Bit](https://fluentbit.io/) and New Relic Log Forwarder.
@@ -169,6 +189,12 @@ Files:
"echo AXIOM_DATASET_NAME={$server->settings->logdrain_axiom_dataset_name} >> $config_path/.env",
"echo AXIOM_API_KEY={$server->settings->logdrain_axiom_api_key} >> $config_path/.env",
];
} else if ($type === 'custom') {
$add_envs_command = [
"touch $config_path/.env"
];
} else {
throw new \Exception('Unknown log drain type.');
}
$restart_command = [
"echo 'Stopping old Fluent Bit'",

View File

@@ -14,15 +14,15 @@ class StartService
$network = $service->destination->network;
$service->saveComposeConfigs();
$commands[] = "cd " . $service->workdir();
$commands[] = "echo '####### Saved configuration files to {$service->workdir()}.'";
$commands[] = "echo '####### Creating Docker network.'";
$commands[] = "docker network create --attachable '{$service->uuid}' >/dev/null || true";
$commands[] = "echo '####### Starting service {$service->name} on {$service->server->name}.'";
$commands[] = "echo '####### Pulling images.'";
$commands[] = "echo 'Saved configuration files to {$service->workdir()}.'";
$commands[] = "echo 'Creating Docker network.'";
$commands[] = "docker network create --attachable '{$service->uuid}' >/dev/null 2>&1 || true";
$commands[] = "echo 'Starting service {$service->name} on {$service->server->name}.'";
$commands[] = "echo 'Pulling images.'";
$commands[] = "docker compose pull";
$commands[] = "echo '####### Starting containers.'";
$commands[] = "docker compose up -d --remove-orphans --force-recreate";
$commands[] = "docker network connect $service->uuid coolify-proxy || true";
$commands[] = "echo 'Starting containers.'";
$commands[] = "docker compose up -d --remove-orphans --force-recreate --build";
$commands[] = "docker network connect $service->uuid coolify-proxy >/dev/null 2>&1 || true";
$compose = data_get($service,'docker_compose',[]);
$serviceNames = data_get(Yaml::parse($compose),'services',[]);
foreach($serviceNames as $serviceName => $serviceConfig){

View File

@@ -30,24 +30,26 @@ class Init extends Command
$this->alive();
$cleanup = $this->option('cleanup');
if ($cleanup) {
echo "Running cleanup\n";
$this->cleanup_stucked_resources();
$this->cleanup_ssh();
// $this->cleanup_ssh();
}
$this->cleanup_in_progress_application_deployments();
$this->cleanup_stucked_helper_containers();
}
private function cleanup_stucked_helper_containers() {
private function cleanup_stucked_helper_containers()
{
$servers = Server::all();
foreach ($servers as $server) {
if ($server->isFunctional()) {
CleanupHelperContainersJob::dispatch($server);
}
}
}
private function alive()
{
$id = config('app.id');
$version = config('version');
$settings = InstanceSettings::get();
$do_not_track = data_get($settings, 'do_not_track');
if ($do_not_track == true) {
@@ -55,27 +57,29 @@ class Init extends Command
return;
}
try {
Http::get("https://get.coollabs.io/coolify/v4/alive?appId=$id&version=$version");
echo "I am alive!\n";
Http::get("https://get.coollabs.io/coolify/v4/alive?appId=$id");
} catch (\Throwable $e) {
echo "Error in alive: {$e->getMessage()}\n";
}
}
private function cleanup_ssh()
{
try {
$files = Storage::allFiles('ssh/keys');
foreach ($files as $file) {
Storage::delete($file);
}
$files = Storage::allFiles('ssh/mux');
foreach ($files as $file) {
Storage::delete($file);
}
} catch (\Throwable $e) {
echo "Error in cleaning ssh: {$e->getMessage()}\n";
}
}
// private function cleanup_ssh()
// {
// TODO: it will cleanup id.root@host.docker.internal
// try {
// $files = Storage::allFiles('ssh/keys');
// foreach ($files as $file) {
// Storage::delete($file);
// }
// $files = Storage::allFiles('ssh/mux');
// foreach ($files as $file) {
// Storage::delete($file);
// }
// } catch (\Throwable $e) {
// echo "Error in cleaning ssh: {$e->getMessage()}\n";
// }
// }
private function cleanup_in_progress_application_deployments()
{
// Cleanup any failed deployments
@@ -97,15 +101,15 @@ class Init extends Command
$applications = Application::all();
foreach ($applications as $application) {
if (!data_get($application, 'environment')) {
ray('Application without environment', $application->name);
$application->delete();
}
if (!data_get($application, 'destination.server')) {
ray('Application without server', $application->name);
echo 'Application without environment' . $application->name . 'deleting\n';
$application->delete();
}
if (!$application->destination()) {
ray('Application without destination', $application->name);
echo 'Application without destination' . $application->name . 'deleting\n';
$application->delete();
}
if (!data_get($application, 'destination.server')) {
echo 'Application without server' . $application->name . 'deleting\n';
$application->delete();
}
}
@@ -116,15 +120,15 @@ class Init extends Command
$postgresqls = StandalonePostgresql::all();
foreach ($postgresqls as $postgresql) {
if (!data_get($postgresql, 'environment')) {
ray('Postgresql without environment', $postgresql->name);
$postgresql->delete();
}
if (!data_get($postgresql, 'destination.server')) {
ray('Postgresql without server', $postgresql->name);
echo 'Postgresql without environment' . $postgresql->name . 'deleting\n';
$postgresql->delete();
}
if (!$postgresql->destination()) {
ray('Postgresql without destination', $postgresql->name);
echo 'Postgresql without destination' . $postgresql->name . 'deleting\n';
$postgresql->delete();
}
if (!data_get($postgresql, 'destination.server')) {
echo 'Postgresql without server' . $postgresql->name . 'deleting\n';
$postgresql->delete();
}
}
@@ -135,15 +139,15 @@ class Init extends Command
$redis = StandaloneRedis::all();
foreach ($redis as $redis) {
if (!data_get($redis, 'environment')) {
ray('Redis without environment', $redis->name);
$redis->delete();
}
if (!data_get($redis, 'destination.server')) {
ray('Redis without server', $redis->name);
echo 'Redis without environment' . $redis->name . 'deleting\n';
$redis->delete();
}
if (!$redis->destination()) {
ray('Redis without destination', $redis->name);
echo 'Redis without destination' . $redis->name . 'deleting\n';
$redis->delete();
}
if (!data_get($redis, 'destination.server')) {
echo 'Redis without server' . $redis->name . 'deleting\n';
$redis->delete();
}
}
@@ -155,15 +159,15 @@ class Init extends Command
$mongodbs = StandaloneMongodb::all();
foreach ($mongodbs as $mongodb) {
if (!data_get($mongodb, 'environment')) {
ray('Mongodb without environment', $mongodb->name);
$mongodb->delete();
}
if (!data_get($mongodb, 'destination.server')) {
ray('Mongodb without server', $mongodb->name);
echo 'Mongodb without environment' . $mongodb->name . 'deleting\n';
$mongodb->delete();
}
if (!$mongodb->destination()) {
ray('Mongodb without destination', $mongodb->name);
echo 'Mongodb without destination' . $mongodb->name . 'deleting\n';
$mongodb->delete();
}
if (!data_get($mongodb, 'destination.server')) {
echo 'Mongodb without server' . $mongodb->name . 'deleting\n';
$mongodb->delete();
}
}
@@ -175,15 +179,15 @@ class Init extends Command
$mysqls = StandaloneMysql::all();
foreach ($mysqls as $mysql) {
if (!data_get($mysql, 'environment')) {
ray('Mysql without environment', $mysql->name);
$mysql->delete();
}
if (!data_get($mysql, 'destination.server')) {
ray('Mysql without server', $mysql->name);
echo 'Mysql without environment' . $mysql->name . 'deleting\n';
$mysql->delete();
}
if (!$mysql->destination()) {
ray('Mysql without destination', $mysql->name);
echo 'Mysql without destination' . $mysql->name . 'deleting\n';
$mysql->delete();
}
if (!data_get($mysql, 'destination.server')) {
echo 'Mysql without server' . $mysql->name . 'deleting\n';
$mysql->delete();
}
}
@@ -195,15 +199,15 @@ class Init extends Command
$mariadbs = StandaloneMariadb::all();
foreach ($mariadbs as $mariadb) {
if (!data_get($mariadb, 'environment')) {
ray('Mariadb without environment', $mariadb->name);
$mariadb->delete();
}
if (!data_get($mariadb, 'destination.server')) {
ray('Mariadb without server', $mariadb->name);
echo 'Mariadb without environment' . $mariadb->name . 'deleting\n';
$mariadb->delete();
}
if (!$mariadb->destination()) {
ray('Mariadb without destination', $mariadb->name);
echo 'Mariadb without destination' . $mariadb->name . 'deleting\n';
$mariadb->delete();
}
if (!data_get($mariadb, 'destination.server')) {
echo 'Mariadb without server' . $mariadb->name . 'deleting\n';
$mariadb->delete();
}
}
@@ -215,15 +219,15 @@ class Init extends Command
$services = Service::all();
foreach ($services as $service) {
if (!data_get($service, 'environment')) {
ray('Service without environment', $service->name);
$service->delete();
}
if (!data_get($service, 'server')) {
ray('Service without server', $service->name);
echo 'Service without environment' . $service->name . 'deleting\n';
$service->delete();
}
if (!$service->destination()) {
ray('Service without destination', $service->name);
echo 'Service without destination' . $service->name . 'deleting\n';
$service->delete();
}
if (!data_get($service, 'server')) {
echo 'Service without server' . $service->name . 'deleting\n';
$service->delete();
}
}
@@ -234,7 +238,7 @@ class Init extends Command
$serviceApplications = ServiceApplication::all();
foreach ($serviceApplications as $service) {
if (!data_get($service, 'service')) {
ray('ServiceApplication without service', $service->name);
echo 'ServiceApplication without service' . $service->name . 'deleting\n';
$service->delete();
}
}
@@ -245,7 +249,7 @@ class Init extends Command
$serviceDatabases = ServiceDatabase::all();
foreach ($serviceDatabases as $service) {
if (!data_get($service, 'service')) {
ray('ServiceDatabase without service', $service->name);
echo 'ServiceDatabase without service' . $service->name . 'deleting\n';
$service->delete();
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace App\Console\Commands;
use App\Models\User;
use Illuminate\Console\Command;
class RootChangeEmail extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'root:change-email';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Change Root Email';
/**
* Execute the console command.
*/
public function handle()
{
//
$this->info('You are about to change the root user\'s email.');
$email = $this->ask('Give me a new email for root user');
$this->info('Updating root email...');
try {
User::find(0)->update(['email' => $email]);
$this->info('Root user\'s email updated successfully.');
} catch (\Exception $e) {
$this->error('Failed to update root user\'s email.');
return;
}
}
}

View File

@@ -8,14 +8,14 @@ use Illuminate\Support\Facades\Hash;
use function Laravel\Prompts\password;
class UsersResetRoot extends Command
class RootResetPassword extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'users:reset-root';
protected $signature = 'root:reset-password';
/**
* The console command description.

View File

@@ -12,21 +12,21 @@ use function Laravel\Prompts\confirm;
use function Laravel\Prompts\multiselect;
use function Laravel\Prompts\select;
class ResourcesDelete extends Command
class ServicesDelete extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'resources:delete';
protected $signature = 'services:delete';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Delete a resource from the database';
protected $description = 'Delete a service from the database';
/**
* Execute the console command.
@@ -34,7 +34,7 @@ class ResourcesDelete extends Command
public function handle()
{
$resource = select(
'What resource do you want to delete?',
'What service do you want to delete?',
['Application', 'Database', 'Service', 'Server'],
);
if ($resource === 'Application') {

View File

@@ -26,7 +26,7 @@ class ServicesGenerate extends Command
*/
public function handle()
{
ray()->clearAll();
// ray()->clearAll();
$files = array_diff(scandir(base_path('templates/compose')), ['.', '..']);
$files = array_filter($files, function ($file) {
return strpos($file, '.yaml') !== false;

View File

@@ -71,6 +71,15 @@ class SyncBunny extends Command
]);
});
try {
if (!$only_template && !$only_version) {
$this->info('About to sync files (docker-compose.prod.yaml, upgrade.sh, install.sh, etc) to BunnyCDN.');
}
if ($only_template) {
$this->info('About to sync service-templates.json to BunnyCDN.');
}
if ($only_version) {
$this->info('About to sync versions.json to BunnyCDN.');
}
$confirmed = confirm('Are you sure you want to sync?');
if (!$confirmed) {
return;

View File

@@ -2,6 +2,7 @@
namespace App\Console;
use App\Jobs\CheckLogDrainContainerJob;
use App\Jobs\CleanupInstanceStuffsJob;
use App\Jobs\DatabaseBackupJob;
use App\Jobs\InstanceAutoUpdateJob;
@@ -11,6 +12,7 @@ use App\Jobs\ServerStatusJob;
use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup;
use App\Models\Server;
use App\Models\Team;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
@@ -43,7 +45,7 @@ class Kernel extends ConsoleKernel
}
private function pull_helper_image($schedule)
{
$servers = Server::all()->where('settings.is_usable', true)->where('settings.is_reachable', true);
$servers = Server::all()->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4');
foreach ($servers as $server) {
$schedule->job(new PullHelperImageJob($server))->everyTenMinutes()->onOneServer();
}
@@ -51,13 +53,18 @@ class Kernel extends ConsoleKernel
private function check_resources($schedule)
{
if (isCloud()) {
$servers = Server::all()->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended', false);
$servers = Server::all()->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended', false)->where('ip', '!=', '1.2.3.4');
$own = Team::find(0)->servers;
$servers = $servers->merge($own);
} else {
$servers = Server::all();
$servers = Server::all()->where('ip', '!=', '1.2.3.4');
}
foreach ($servers as $server) {
$schedule->job(new ServerStatusJob($server))->everyTenMinutes()->onOneServer();
$schedule->job(new ContainerStatusJob($server))->everyMinute()->onOneServer();
if ($server->isLogDrainEnabled()) {
$schedule->job(new CheckLogDrainContainerJob($server))->everyMinute()->onOneServer();
}
}
}
private function instance_auto_update($schedule)

View File

@@ -6,6 +6,7 @@ use App\Models\InstanceSettings;
use App\Models\User;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use RuntimeException;
use Sentry\Laravel\Integration;
use Sentry\State\Scope;
use Throwable;
@@ -55,7 +56,9 @@ class Handler extends ExceptionHandler
{
$this->reportable(function (Throwable $e) {
if (isDev()) {
ray($e);
// return;
}
if ($e instanceof RuntimeException) {
return;
}
$this->settings = InstanceSettings::get();
@@ -74,6 +77,7 @@ class Handler extends ExceptionHandler
);
}
);
ray('reporting to sentry');
Integration::captureUnhandledException($e);
});
}

View File

@@ -10,23 +10,6 @@ class ApplicationController extends Controller
{
use AuthorizesRequests, ValidatesRequests;
public function configuration()
{
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}
$environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']);
if (!$environment) {
return redirect()->route('dashboard');
}
$application = $environment->applications->where('uuid', request()->route('application_uuid'))->first();
if (!$application) {
return redirect()->route('dashboard');
}
return view('project.application.configuration', ['application' => $application]);
}
public function deployments()
{
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();

View File

@@ -67,7 +67,7 @@ class ProjectController extends Controller
$database = create_standalone_mongodb($environment->id, $destination_uuid);
} else if ($type->value() === 'mysql') {
$database = create_standalone_mysql($environment->id, $destination_uuid);
}else if ($type->value() === 'mariadb') {
} else if ($type->value() === 'mariadb') {
$database = create_standalone_mariadb($environment->id, $destination_uuid);
}
return redirect()->route('project.database.configuration', [
@@ -104,27 +104,7 @@ class ProjectController extends Controller
$generatedValue = $value;
if ($value->contains('SERVICE_')) {
$command = $value->after('SERVICE_')->beforeLast('_');
// TODO: make it shared with Service.php
switch ($command->value()) {
case 'PASSWORD':
$generatedValue = Str::password(symbols: false);
break;
case 'PASSWORD_64':
$generatedValue = Str::password(length: 64, symbols: false);
break;
case 'BASE64_64':
$generatedValue = Str::random(64);
break;
case 'BASE64_128':
$generatedValue = Str::random(128);
break;
case 'BASE64':
$generatedValue = Str::random(32);
break;
case 'USER':
$generatedValue = Str::random(16);
break;
}
$generatedValue = generateEnvValue($command->value());
}
EnvironmentVariable::create([
'key' => $key,
@@ -137,7 +117,7 @@ class ProjectController extends Controller
}
$service->parse(isNew: true);
return redirect()->route('project.service', [
return redirect()->route('project.service.configuration', [
'service_uuid' => $service->uuid,
'environment_name' => $environment->name,
'project_uuid' => $project->uuid,

View File

@@ -31,6 +31,8 @@ class Index extends Component
public ?string $remoteServerHost = null;
public ?int $remoteServerPort = 22;
public ?string $remoteServerUser = 'root';
public bool $isSwarmManager = false;
public bool $isCloudflareTunnel = false;
public ?Server $createdServer = null;
public Collection $projects;
@@ -182,13 +184,15 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
'private_key_id' => $this->createdPrivateKey->id,
'team_id' => currentTeam()->id,
]);
$this->createdServer->save();
$this->createdServer->settings->is_swarm_manager = $this->isSwarmManager;
$this->createdServer->settings->is_cloudflare_tunnel = $this->isCloudflareTunnel;
$this->createdServer->settings->save();
$this->createdServer->addInitialNetwork();
$this->validateServer();
}
public function validateServer()
{
try {
$customErrorMessage = "Server is not reachable:";
config()->set('coolify.mux_enabled', false);
instant_remote_process(['uptime'], $this->createdServer, true);
@@ -198,7 +202,8 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
]);
} catch (\Throwable $e) {
$this->serverReachable = false;
return handleError(error: $e, customErrorMessage: $customErrorMessage, livewire: $this);
$this->createdServer->delete();
return handleError(error: $e, livewire: $this);
}
try {
@@ -206,7 +211,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$dockerVersion = checkMinimumDockerEngineVersion($dockerVersion);
if (is_null($dockerVersion)) {
$this->currentState = 'install-docker';
throw new \Exception('Docker version is not supported or not installed.');
throw new \Exception('Docker not found or old version is installed.');
}
$this->createdServer->settings()->update([
'is_usable' => true,
@@ -214,14 +219,20 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$this->getProxyType();
} catch (\Throwable $e) {
// $this->dockerInstallationStarted = false;
return handleError(error: $e, customErrorMessage: $customErrorMessage, livewire: $this);
return handleError(error: $e, livewire: $this);
}
}
public function installDocker()
{
$this->dockerInstallationStarted = true;
$activity = InstallDocker::run($this->createdServer);
$this->emit('newMonitorActivity', $activity->id);
try {
$this->dockerInstallationStarted = true;
$activity = InstallDocker::run($this->createdServer);
$this->emit('installDocker');
$this->emit('newMonitorActivity', $activity->id);
} catch (\Throwable $e) {
$this->dockerInstallationStarted = false;
return handleError(error: $e, livewire: $this);
}
}
public function dockerInstalledOrSkipped()
{

View File

@@ -3,7 +3,6 @@
namespace App\Http\Livewire;
use App\Models\Project;
use App\Models\S3Storage;
use App\Models\Server;
use Livewire\Component;

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Http\Livewire\Project\Application;
use App\Models\Application;
use Livewire\Component;
class Advanced extends Component
{
public Application $application;
protected $rules = [
'application.settings.is_git_submodules_enabled' => 'boolean|required',
'application.settings.is_git_lfs_enabled' => 'boolean|required',
'application.settings.is_preview_deployments_enabled' => 'boolean|required',
'application.settings.is_auto_deploy_enabled' => 'boolean|required',
'application.settings.is_force_https_enabled' => 'boolean|required',
'application.settings.is_log_drain_enabled' => 'boolean|required',
'application.settings.is_gpu_enabled' => 'boolean|required',
'application.settings.gpu_driver' => 'string|required',
'application.settings.gpu_count' => 'string|required',
'application.settings.gpu_device_ids' => 'string|required',
'application.settings.gpu_options' => 'string|required',
];
public function instantSave()
{
if ($this->application->isLogDrainEnabled()) {
if (!$this->application->destination->server->isLogDrainEnabled()) {
$this->application->settings->is_log_drain_enabled = false;
$this->emit('error', 'Log drain is not enabled on this server.');
return;
}
}
if ($this->application->settings->is_force_https_enabled) {
$this->emit('resetDefaultLabels', false);
}
$this->application->settings->save();
$this->emit('success', 'Settings saved.');
}
public function submit() {
if ($this->application->settings->gpu_count && $this->application->settings->gpu_device_ids) {
$this->emit('error', 'You cannot set both GPU count and GPU device IDs.');
$this->application->settings->gpu_count = null;
$this->application->settings->gpu_device_ids = null;
$this->application->settings->save();
return;
}
$this->application->settings->save();
$this->emit('success', 'Settings saved.');
}
public function render()
{
return view('livewire.project.application.advanced');
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Http\Livewire\Project\Application;
use App\Models\Application;
use App\Models\Server;
use App\Models\StandaloneDocker;
use Livewire\Component;
class Configuration extends Component
{
public Application $application;
public $servers;
public function mount()
{
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}
$environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']);
if (!$environment) {
return redirect()->route('dashboard');
}
$application = $environment->applications->where('uuid', request()->route('application_uuid'))->first();
if (!$application) {
return redirect()->route('dashboard');
}
$this->application = $application;
$mainServer = $application->destination->server;
$servers = Server::ownedByCurrentTeam()->get();
$this->servers = $servers->filter(function ($server) use ($mainServer) {
return $server->id != $mainServer->id;
});
}
public function render()
{
return view('livewire.project.application.configuration');
}
}

View File

@@ -6,6 +6,7 @@ use App\Models\Application;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class General extends Component
{
@@ -25,15 +26,17 @@ class General extends Component
public bool $labelsChanged = false;
public bool $isConfigurationChanged = false;
public ?string $initialDockerComposeLocation = null;
public ?string $initialDockerComposePrLocation = null;
public bool $is_static;
public bool $is_git_submodules_enabled;
public bool $is_git_lfs_enabled;
public bool $is_debug_enabled;
public bool $is_preview_deployments_enabled;
public bool $is_auto_deploy_enabled;
public bool $is_force_https_enabled;
public $parsedServices = [];
public $parsedServiceDomains = [];
protected $listeners = [
'resetDefaultLabels'
];
protected $rules = [
'application.name' => 'required',
'application.description' => 'nullable',
@@ -54,8 +57,15 @@ class General extends Component
'application.docker_registry_image_name' => 'nullable',
'application.docker_registry_image_tag' => 'nullable',
'application.dockerfile_location' => 'nullable',
'application.docker_compose_location' => 'nullable',
'application.docker_compose_pr_location' => 'nullable',
'application.docker_compose' => 'nullable',
'application.docker_compose_pr' => 'nullable',
'application.docker_compose_raw' => 'nullable',
'application.docker_compose_pr_raw' => 'nullable',
'application.custom_labels' => 'nullable',
'application.dockerfile_target_build' => 'nullable',
'application.settings.is_static' => 'boolean|required',
];
protected $validationAttributes = [
'application.name' => 'name',
@@ -77,12 +87,26 @@ class General extends Component
'application.docker_registry_image_name' => 'Docker registry image name',
'application.docker_registry_image_tag' => 'Docker registry image tag',
'application.dockerfile_location' => 'Dockerfile location',
'application.docker_compose_location' => 'Docker compose location',
'application.docker_compose_pr_location' => 'Docker compose location',
'application.docker_compose' => 'Docker compose',
'application.docker_compose_pr' => 'Docker compose',
'application.docker_compose_raw' => 'Docker compose raw',
'application.docker_compose_pr_raw' => 'Docker compose raw',
'application.custom_labels' => 'Custom labels',
'application.dockerfile_target_build' => 'Dockerfile target build',
'application.settings.is_static' => 'Is static',
];
public function mount()
{
try {
$this->parsedServices = $this->application->parseCompose();
} catch (\Throwable $e) {
$this->emit('error', $e->getMessage());
}
$this->parsedServiceDomains = $this->application->docker_compose_domains ? json_decode($this->application->docker_compose_domains, true) : [];
$this->ports_exposes = $this->application->ports_exposes;
if (str($this->application->status)->startsWith('running') && is_null($this->application->config_hash)) {
$this->application->isConfigurationChanged(true);
@@ -93,23 +117,52 @@ class General extends Component
} else {
$this->customLabels = str($this->application->custom_labels)->replace(',', "\n");
}
if (data_get($this->application, 'settings')) {
$this->is_static = $this->application->settings->is_static;
$this->is_git_submodules_enabled = $this->application->settings->is_git_submodules_enabled;
$this->is_git_lfs_enabled = $this->application->settings->is_git_lfs_enabled;
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
$this->is_preview_deployments_enabled = $this->application->settings->is_preview_deployments_enabled;
$this->is_auto_deploy_enabled = $this->application->settings->is_auto_deploy_enabled;
$this->is_force_https_enabled = $this->application->settings->is_force_https_enabled;
}
$this->initialDockerComposeLocation = $this->application->docker_compose_location;
$this->checkLabelUpdates();
}
public function instantSave()
{
$this->application->settings->save();
$this->emit('success', 'Settings saved.');
}
public function loadComposeFile($isInit = false)
{
try {
if ($isInit && $this->application->docker_compose_raw) {
return;
}
['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation, 'initialDockerComposePrLocation' => $this->initialDockerComposePrLocation] = $this->application->loadComposeFile($isInit);
$this->emit('success', 'Docker compose file loaded.');
} catch (\Throwable $e) {
$this->application->docker_compose_location = $this->initialDockerComposeLocation;
$this->application->docker_compose_pr_location = $this->initialDockerComposePrLocation;
$this->application->save();
return handleError($e, $this);
}
}
public function generateDomain(string $serviceName)
{
$domain = $this->parsedServiceDomains[$serviceName]['domain'] ?? null;
if (!$domain) {
$uuid = new Cuid2(7);
$domain = generateFqdn($this->application->destination->server, $uuid);
$this->parsedServiceDomains[$serviceName]['domain'] = $domain;
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
$this->application->save();
$this->emit('success', 'Domain generated.');
}
return $domain;
}
public function updatedApplicationBuildPack()
{
if ($this->application->build_pack !== 'nixpacks') {
$this->application->settings->is_static = $this->is_static = false;
$this->application->settings->save();
}
if ($this->application->build_pack === 'dockercompose') {
$this->application->fqdn = null;
$this->application->settings->save();
}
$this->submit();
}
public function checkLabelUpdates()
@@ -120,32 +173,6 @@ class General extends Component
$this->labelsChanged = false;
}
}
public function instantSave()
{
// @TODO: find another way - if possible
$force_https = $this->application->settings->is_force_https_enabled;
$this->application->settings->is_static = $this->is_static;
if ($this->is_static) {
$this->application->ports_exposes = 80;
} else {
$this->application->ports_exposes = 3000;
}
$this->application->settings->is_git_submodules_enabled = $this->is_git_submodules_enabled;
$this->application->settings->is_git_lfs_enabled = $this->is_git_lfs_enabled;
$this->application->settings->is_debug_enabled = $this->is_debug_enabled;
$this->application->settings->is_preview_deployments_enabled = $this->is_preview_deployments_enabled;
$this->application->settings->is_auto_deploy_enabled = $this->is_auto_deploy_enabled;
$this->application->settings->is_force_https_enabled = $this->is_force_https_enabled;
$this->application->settings->save();
$this->application->save();
$this->application->refresh();
$this->emit('success', 'Application settings updated!');
$this->checkLabelUpdates();
$this->isConfigurationChanged = $this->application->isConfigurationChanged();
if ($force_https !== $this->is_force_https_enabled) {
$this->resetDefaultLabels(false);
}
}
public function getWildcardDomain()
{
@@ -172,6 +199,9 @@ class General extends Component
public function submit($showToaster = true)
{
try {
if ($this->application->build_pack === 'dockercompose' && ($this->initialDockerComposeLocation !== $this->application->docker_compose_location || $this->initialDockerComposePrLocation !== $this->application->docker_compose_pr_location)) {
$this->loadComposeFile();
}
$this->validate();
if ($this->ports_exposes !== $this->application->ports_exposes) {
$this->resetDefaultLabels(false);
@@ -204,6 +234,10 @@ class General extends Component
$this->customLabels = str($this->customLabels)->replace(',', "\n");
}
$this->application->custom_labels = $this->customLabels->explode("\n")->implode(',');
if ($this->application->build_pack === 'dockercompose') {
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
$this->parsedServices = $this->application->parseCompose();
}
$this->application->save();
$showToaster && $this->emit('success', 'Application settings updated!');
} catch (\Throwable $e) {

View File

@@ -4,6 +4,7 @@ namespace App\Http\Livewire\Project\Application;
use App\Actions\Application\StopApplication;
use App\Jobs\ContainerStatusJob;
use App\Jobs\ServerStatusJob;
use App\Models\Application;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
@@ -28,6 +29,8 @@ class Heading extends Component
$this->application->previews->each(function ($preview) {
$preview->refresh();
});
} else {
dispatch(new ServerStatusJob($this->application->destination->server));
}
}
@@ -38,6 +41,10 @@ class Heading extends Component
public function deploy(bool $force_rebuild = false)
{
if ($this->application->build_pack === 'dockercompose' && is_null($this->application->docker_compose_raw)) {
$this->emit('error', 'Please load a Compose file first.');
return;
}
$this->setDeploymentUuid();
queue_application_deployment(
application_id: $this->application->id,
@@ -65,7 +72,8 @@ class Heading extends Component
$this->application->save();
$this->application->refresh();
}
public function restart() {
public function restart()
{
$this->setDeploymentUuid();
queue_application_deployment(
application_id: $this->application->id,

View File

@@ -72,10 +72,12 @@ class Previews extends Component
public function stop(int $pull_request_id)
{
try {
$container_name = generateApplicationContainerName($this->application, $pull_request_id);
instant_remote_process(["docker rm -f $container_name"], $this->application->destination->server, throwError: false);
ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->delete();
$containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id, $pull_request_id);
foreach ($containers as $container) {
$name = str_replace('/', '', $container['Names']);
instant_remote_process(["docker rm -f $name"], $this->application->destination->server, throwError: false);
}
ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first()->delete();
$this->application->refresh();
} catch (\Throwable $e) {
return handleError($e, $this);

View File

@@ -38,32 +38,36 @@ class Rollback extends Component
]);
}
public function loadImages()
public function loadImages($showToast = false)
{
try {
$image = $this->application->uuid;
$output = instant_remote_process([
"docker inspect --format='{{.Config.Image}}' {$this->application->uuid}",
], $this->application->destination->server, throwError: false);
$current_tag = Str::of($output)->trim()->explode(":");
$this->current = data_get($current_tag, 1);
$image = $this->application->docker_registry_image_name ?? $this->application->uuid;
if ($this->application->destination->server->isFunctional()) {
$output = instant_remote_process([
"docker inspect --format='{{.Config.Image}}' {$this->application->uuid}",
], $this->application->destination->server, throwError: false);
$current_tag = Str::of($output)->trim()->explode(":");
$this->current = data_get($current_tag, 1);
$output = instant_remote_process([
"docker images --format '{{.Repository}}#{{.Tag}}#{{.CreatedAt}}'",
], $this->application->destination->server);
$this->images = Str::of($output)->trim()->explode("\n")->filter(function ($item) use ($image) {
return Str::of($item)->contains($image);
})->map(function ($item) {
$item = Str::of($item)->explode('#');
if ($item[1] === $this->current) {
// $is_current = true;
}
return [
'tag' => $item[1],
'created_at' => $item[2],
'is_current' => $is_current ?? null,
];
})->toArray();
$output = instant_remote_process([
"docker images --format '{{.Repository}}#{{.Tag}}#{{.CreatedAt}}'",
], $this->application->destination->server);
$this->images = Str::of($output)->trim()->explode("\n")->filter(function ($item) use ($image) {
return Str::of($item)->contains($image);
})->map(function ($item) {
$item = Str::of($item)->explode('#');
if ($item[1] === $this->current) {
// $is_current = true;
}
return [
'tag' => $item[1],
'created_at' => $item[2],
'is_current' => $is_current ?? null,
];
})->toArray();
}
$showToast && $this->emit('success', 'Images loaded.');
return [];
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -28,6 +28,7 @@ class General extends Component
'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
];
protected $validationAttributes = [
'database.name' => 'Name',
@@ -50,6 +51,20 @@ class General extends Component
$this->db_url_public = $this->database->getDbUrl();
}
}
public function instantSaveAdvanced() {
try {
if (!$this->database->destination->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->emit('error', 'Log drain is not enabled on the server. Please enable it first.');
return;
}
$this->database->save();
$this->emit('success', 'Database updated successfully.');
$this->emit('success', 'You need to restart the service for the changes to take effect.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function submit()
{
try {

View File

@@ -27,6 +27,7 @@ class General extends Component
'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
];
protected $validationAttributes = [
'database.name' => 'Name',
@@ -48,7 +49,21 @@ class General extends Component
$this->db_url_public = $this->database->getDbUrl();
}
}
public function instantSaveAdvanced()
{
try {
if (!$this->database->destination->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->emit('error', 'Log drain is not enabled on the server. Please enable it first.');
return;
}
$this->database->save();
$this->emit('success', 'Database updated successfully.');
$this->emit('success', 'You need to restart the service for the changes to take effect.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function submit()
{
try {

View File

@@ -28,6 +28,7 @@ class General extends Component
'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
];
protected $validationAttributes = [
'database.name' => 'Name',
@@ -50,6 +51,21 @@ class General extends Component
$this->db_url_public = $this->database->getDbUrl();
}
}
public function instantSaveAdvanced()
{
try {
if (!$this->database->destination->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->emit('error', 'Log drain is not enabled on the server. Please enable it first.');
return;
}
$this->database->save();
$this->emit('success', 'Database updated successfully.');
$this->emit('success', 'You need to restart the service for the changes to take effect.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function submit()
{
try {

View File

@@ -34,6 +34,7 @@ class General extends Component
'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
];
protected $validationAttributes = [
'database.name' => 'Name',
@@ -57,6 +58,20 @@ class General extends Component
$this->db_url_public = $this->database->getDbUrl();
}
}
public function instantSaveAdvanced() {
try {
if (!$this->database->destination->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->emit('error', 'Log drain is not enabled on the server. Please enable it first.');
return;
}
$this->database->save();
$this->emit('success', 'Database updated successfully.');
$this->emit('success', 'You need to restart the service for the changes to take effect.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function instantSave()
{
try {

View File

@@ -25,6 +25,7 @@ class General extends Component
'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
];
protected $validationAttributes = [
'database.name' => 'Name',
@@ -43,6 +44,20 @@ class General extends Component
$this->db_url_public = $this->database->getDbUrl();
}
}
public function instantSaveAdvanced() {
try {
if (!$this->database->destination->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->emit('error', 'Log drain is not enabled on the server. Please enable it first.');
return;
}
$this->database->save();
$this->emit('success', 'Database updated successfully.');
$this->emit('success', 'You need to restart the service for the changes to take effect.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function submit()
{
try {

View File

@@ -22,79 +22,19 @@ class DockerCompose extends Component
$this->query = request()->query();
if (isDev()) {
$this->dockerComposeRaw = 'services:
ghost:
image: ghost:5
volumes:
- ~/configs:/etc/configs/:ro
- ./var/lib/ghost/content:/tmp/ghost2/content:ro
- /var/lib/ghost/content:/tmp/ghost/content:rw
- ghost-content-data:/var/lib/ghost/content
- type: volume
source: mydata
target: /data
- type: bind
source: ./var/lib/ghost/data
target: /data
- type: bind
source: /tmp
target: /tmp
labels:
- "test.label=true"
ports:
- "3000"
- "3000-3005"
- "8000:8000"
- "9090-9091:8080-8081"
- "49100:22"
- "127.0.0.1:8001:8001"
- "127.0.0.1:5000-5010:5000-5010"
- "127.0.0.1::5000"
- "6060:6060/udp"
- "12400-12500:1240"
- target: 80
published: 8080
protocol: tcp
mode: host
networks:
- some-network
- other-network
appsmith:
build:
context: .
dockerfile_inline: |
FROM nginx
ARG GIT_COMMIT
ARG GIT_BRANCH
RUN echo "Hello World ${GIT_COMMIT} ${GIT_BRANCH}"
args:
- GIT_COMMIT=cdc3b19
- GIT_BRANCH=${GIT_BRANCH}
environment:
- database__client=${DATABASE_CLIENT:-mysql}
- database__connection__database=${MYSQL_DATABASE:-ghost}
- database__connection__host=${DATABASE_CONNECTION_HOST:-mysql}
- test=${TEST:?true}
- url=$SERVICE_FQDN_GHOST
- database__connection__user=$SERVICE_USER_MYSQL
- database__connection__password=$SERVICE_PASSWORD_MYSQL
depends_on:
- mysql
mysql:
image: mysql:8.0
volumes:
- ghost-mysql-data:/var/lib/mysql
environment:
- MYSQL_USER=${SERVICE_USER_MYSQL}
- MYSQL_PASSWORD=${SERVICE_PASSWORD_MYSQL}
- MYSQL_DATABASE=$MYSQL_DATABASE
- MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_MYSQLROOT}
- SESSION_SECRET
minio:
image: minio/minio
environment:
RACK_ENV: development
A: $A
SHOW: ${SHOW}
SHOW1: ${SHOW2-show1}
SHOW2: ${SHOW3:-show2}
SHOW3: ${SHOW4?show3}
SHOW4: ${SHOW5:?show4}
SHOW5: ${SERVICE_USER_MINIO}
SHOW6: ${SERVICE_PASSWORD_MINIO}
SHOW7: ${SERVICE_PASSWORD_64_MINIO}
SHOW8: ${SERVICE_BASE64_64_MINIO}
SHOW9: ${SERVICE_BASE64_128_MINIO}
SHOW10: ${SERVICE_BASE64_MINIO}
SHOW11:
- APPSMITH_MAIL_ENABLED=${APPSMITH_MAIL_ENABLED}
';
}
}
@@ -129,7 +69,7 @@ class DockerCompose extends Component
$service->parse(isNew: true);
return redirect()->route('project.service', [
return redirect()->route('project.service.configuration', [
'service_uuid' => $service->uuid,
'environment_name' => $environment->name,
'project_uuid' => $project->uuid,

View File

@@ -47,7 +47,7 @@ class Select extends Component
}
public function render()
{
$this->loadServices();
if ($this->search) $this->loadServices();
return view('livewire.project.new.select');
}
@@ -69,10 +69,10 @@ class Select extends Component
// }
// }
public function loadServices(bool $force = false)
public function loadServices()
{
try {
if (count($this->allServices) > 0 && !$force) {
if (count($this->allServices) > 0) {
if (!$this->search) {
$this->services = $this->allServices;
return;

View File

@@ -16,6 +16,7 @@ class Application extends Component
'application.image' => 'required',
'application.exclude_from_status' => 'required|boolean',
'application.required_fqdn' => 'required|boolean',
'application.is_log_drain_enabled' => 'nullable|boolean',
];
public function render()
{
@@ -25,13 +26,22 @@ class Application extends Component
{
$this->submit();
}
public function instantSaveAdvanced()
{
if (!$this->application->service->destination->server->isLogDrainEnabled()) {
$this->application->is_log_drain_enabled = false;
$this->emit('error', 'Log drain is not enabled on the server. Please enable it first.');
return;
}
$this->application->save();
$this->emit('success', 'You need to restart the service for the changes to take effect.');
}
public function delete()
{
try {
$this->application->delete();
$this->emit('success', 'Application deleted successfully.');
return redirect()->route('project.service', $this->parameters);
return redirect()->route('project.service.configuration', $this->parameters);
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -21,18 +21,31 @@ class Database extends Component
'database.exclude_from_status' => 'required|boolean',
'database.public_port' => 'nullable|integer',
'database.is_public' => 'required|boolean',
'database.is_log_drain_enabled' => 'required|boolean',
];
public function render()
{
return view('livewire.project.service.database');
}
public function mount() {
public function mount()
{
if ($this->database->is_public) {
$this->db_url_public = $this->database->getServiceDatabaseUrl();
}
$this->refreshFileStorages();
}
public function instantSave() {
public function instantSaveAdvanced()
{
if (!$this->database->service->destination->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->emit('error', 'Log drain is not enabled on the server. Please enable it first.');
return;
}
$this->submit();
$this->emit('success', 'You need to restart the service for the changes to take effect.');
}
public function instantSave()
{
if ($this->database->is_public && !$this->database->public_port) {
$this->emit('error', 'Public port is required.');
$this->database->is_public = false;

View File

@@ -28,7 +28,7 @@ class Index extends Component
}
public function checkStatus()
{
dispatch_sync(new ContainerStatusJob($this->service->server));
dispatch(new ContainerStatusJob($this->service->server));
$this->refreshStacks();
}
public function refreshStacks()

View File

@@ -23,7 +23,7 @@ class StackForm extends Component
foreach ($fields as $fieldKey => $field) {
$key = data_get($field, 'key');
$value = data_get($field, 'value');
$rules = data_get($field, 'rules');
$rules = data_get($field, 'rules', 'nullable');
$isPassword = data_get($field, 'isPassword');
$this->fields[$key] = [
"serviceName" => $serviceName,
@@ -31,6 +31,7 @@ class StackForm extends Component
"name" => $fieldKey,
"value" => $value,
"isPassword" => $isPassword,
"rules" => $rules
];
$this->rules["fields.$key.value"] = $rules;
$this->validationAttributes["fields.$key.value"] = $fieldKey;

View File

@@ -6,5 +6,7 @@ use Livewire\Component;
class Destination extends Component
{
public $destination;
public $resource;
public $servers = [];
public $additionalServers = [];
}

View File

@@ -2,7 +2,16 @@
namespace App\Http\Livewire\Project\Shared;
use App\Models\Application;
use App\Models\Server;
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Illuminate\Support\Facades\Process;
use Livewire\Component;
@@ -10,17 +19,44 @@ class GetLogs extends Component
{
public string $outputs = '';
public string $errors = '';
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $resource;
public ServiceApplication|ServiceDatabase|null $servicesubtype = null;
public Server $server;
public ?string $container = null;
public ?bool $streamLogs = false;
public ?bool $showTimeStamps = true;
public int $numberOfLines = 100;
public function mount()
{
if ($this->resource->getMorphClass() === 'App\Models\Application') {
$this->showTimeStamps = $this->resource->settings->is_include_timestamps;
} else {
if ($this->servicesubtype) {
$this->showTimeStamps = $this->servicesubtype->is_include_timestamps;
} else {
$this->showTimeStamps = $this->resource->is_include_timestamps;
}
}
}
public function doSomethingWithThisChunkOfOutput($output)
{
$this->outputs .= removeAnsiColors($output);
}
public function instantSave()
{
if ($this->resource->getMorphClass() === 'App\Models\Application') {
$this->resource->settings->is_include_timestamps = $this->showTimeStamps;
$this->resource->settings->save();
} else {
if ($this->servicesubtype) {
$this->servicesubtype->is_include_timestamps = $this->showTimeStamps;
$this->servicesubtype->save();
} else {
$this->resource->is_include_timestamps = $this->showTimeStamps;
$this->resource->save();
}
}
}
public function getLogs($refresh = false)
{

View File

@@ -17,13 +17,16 @@ class Logs extends Component
public ?string $type = null;
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $resource;
public Server $server;
public ?string $container = null;
public $container = [];
public $containers;
public $parameters;
public $query;
public $status;
public $serviceSubType;
public function mount()
{
$this->containers = collect();
$this->parameters = get_route_parameters();
$this->query = request()->query();
if (data_get($this->parameters, 'application_uuid')) {
@@ -33,7 +36,9 @@ class Logs extends Component
$this->server = $this->resource->destination->server;
$containers = getCurrentApplicationContainerStatus($this->server, $this->resource->id, 0);
if ($containers->count() > 0) {
$this->container = data_get($containers[0], 'Names');
$containers->each(function ($container) {
$this->containers->push(str_replace('/', '', $container['Names']));
});
}
} else if (data_get($this->parameters, 'database_uuid')) {
$this->type = 'database';
@@ -60,6 +65,11 @@ class Logs extends Component
} else if (data_get($this->parameters, 'service_uuid')) {
$this->type = 'service';
$this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail();
$service_name = data_get($this->parameters, 'service_name');
$this->serviceSubType = $this->resource->applications()->where('name', $service_name)->first();
if (!$this->serviceSubType) {
$this->serviceSubType = $this->resource->databases()->where('name', $service_name)->first();
}
$this->status = $this->resource->status;
$this->server = $this->resource->server;
$this->container = data_get($this->parameters, 'service_name') . '-' . $this->resource->uuid;

View File

@@ -17,14 +17,15 @@ class Form extends Component
protected $listeners = ['serverRefresh'];
protected $rules = [
'server.name' => 'required|min:6',
'server.name' => 'required',
'server.description' => 'nullable',
'server.ip' => 'required',
'server.user' => 'required',
'server.port' => 'required',
'server.settings.is_cloudflare_tunnel' => 'required',
'server.settings.is_cloudflare_tunnel' => 'required|boolean',
'server.settings.is_reachable' => 'required',
'server.settings.is_part_of_swarm' => 'required',
'server.settings.is_swarm_manager' => 'required|boolean',
// 'server.settings.is_swarm_worker' => 'required|boolean',
'wildcard_domain' => 'nullable|url',
];
protected $validationAttributes = [
@@ -34,8 +35,9 @@ class Form extends Component
'server.user' => 'User',
'server.port' => 'Port',
'server.settings.is_cloudflare_tunnel' => 'Cloudflare Tunnel',
'server.settings.is_reachable' => 'is reachable',
'server.settings.is_part_of_swarm' => 'is part of swarm'
'server.settings.is_reachable' => 'Is reachable',
'server.settings.is_swarm_manager' => 'Swarm Manager',
// 'server.settings.is_swarm_worker' => 'Swarm Worker',
];
public function mount()
@@ -43,15 +45,20 @@ class Form extends Component
$this->wildcard_domain = $this->server->settings->wildcard_domain;
$this->cleanup_after_percentage = $this->server->settings->cleanup_after_percentage;
}
public function serverRefresh()
public function serverRefresh($install = true)
{
$this->validateServer();
$this->validateServer($install);
}
public function instantSave()
{
refresh_server_connection($this->server->privateKey);
$this->validateServer();
$this->server->settings->save();
try {
refresh_server_connection($this->server->privateKey);
$this->validateServer(false);
$this->server->settings->save();
$this->emit('success', 'Server updated successfully.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function installDocker()
{
@@ -77,12 +84,15 @@ class Form extends Component
{
try {
$uptime = $this->server->validateConnection();
if ($uptime) {
$install && $this->emit('success', 'Server is reachable.');
} else {
if (!$uptime) {
$install && $this->emit('error', 'Server is not reachable. Please check your connection and configuration.');
return;
}
$supported_os_type = $this->server->validateOS();
if (!$supported_os_type) {
$install && $this->emit('error', 'Server OS type is not supported for automated installation. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://coolify.io/docs/servers#install-docker-engine-manually">documentation</a>.');
return;
}
$dockerInstalled = $this->server->validateDockerEngine();
if ($dockerInstalled) {
$install && $this->emit('success', 'Docker Engine is installed.<br> Checking version.');
@@ -92,11 +102,17 @@ class Form extends Component
}
$dockerVersion = $this->server->validateDockerEngineVersion();
if ($dockerVersion) {
$install && $this->emit('success', 'Docker Engine version is 23+.');
$install && $this->emit('success', 'Docker Engine version is 22+.');
} else {
$install && $this->installDocker();
return;
}
if ($this->server->isSwarm()) {
$swarmInstalled = $this->server->validateDockerSwarm();
if ($swarmInstalled) {
$install && $this->emit('success', 'Docker Swarm is initiated.');
}
}
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {

View File

@@ -2,6 +2,7 @@
namespace App\Http\Livewire\Server;
use App\Actions\Server\InstallLogDrain;
use App\Models\Server;
use Livewire\Component;
@@ -18,6 +19,9 @@ class LogDrains extends Component
'server.settings.is_logdrain_axiom_enabled' => 'required|boolean',
'server.settings.logdrain_axiom_dataset_name' => 'required|string',
'server.settings.logdrain_axiom_api_key' => 'required|string',
'server.settings.is_logdrain_custom_enabled' => 'required|boolean',
'server.settings.logdrain_custom_config' => 'required|string',
'server.settings.logdrain_custom_config_parser' => 'nullable',
];
protected $validationAttributes = [
'server.settings.is_logdrain_newrelic_enabled' => 'New Relic log drain',
@@ -28,6 +32,9 @@ class LogDrains extends Component
'server.settings.is_logdrain_axiom_enabled' => 'Axiom log drain',
'server.settings.logdrain_axiom_dataset_name' => 'Axiom dataset name',
'server.settings.logdrain_axiom_api_key' => 'Axiom API key',
'server.settings.is_logdrain_custom_enabled' => 'Custom log drain',
'server.settings.logdrain_custom_config' => 'Custom log drain configuration',
'server.settings.logdrain_custom_config_parser' => 'Custom log drain configuration parser',
];
public function mount()
@@ -46,14 +53,8 @@ class LogDrains extends Component
public function configureLogDrain()
{
try {
if ($this->server->settings->is_logdrain_newrelic_enabled) {
$this->server->logDrain('newrelic');
} else if ($this->server->settings->is_logdrain_highlight_enabled) {
$this->server->logDrain('highlight');
} else if ($this->server->settings->is_logdrain_axiom_enabled) {
$this->server->logDrain('axiom');
} else {
$this->server->logDrain('none');
InstallLogDrain::run($this->server);
if (!$this->server->isLogDrainEnabled()) {
$this->emit('serverRefresh');
$this->emit('success', 'Log drain service stopped.');
return;
@@ -89,6 +90,7 @@ class LogDrains extends Component
$this->server->settings->update([
'is_logdrain_highlight_enabled' => false,
'is_logdrain_axiom_enabled' => false,
'is_logdrain_custom_enabled' => false,
]);
} else if ($type === 'highlight') {
$this->validate([
@@ -98,6 +100,7 @@ class LogDrains extends Component
$this->server->settings->update([
'is_logdrain_newrelic_enabled' => false,
'is_logdrain_axiom_enabled' => false,
'is_logdrain_custom_enabled' => false,
]);
} else if ($type === 'axiom') {
$this->validate([
@@ -108,6 +111,18 @@ class LogDrains extends Component
$this->server->settings->update([
'is_logdrain_newrelic_enabled' => false,
'is_logdrain_highlight_enabled' => false,
'is_logdrain_custom_enabled' => false,
]);
} else if ($type === 'custom') {
$this->validate([
'server.settings.is_logdrain_custom_enabled' => 'required|boolean',
'server.settings.logdrain_custom_config' => 'required|string',
'server.settings.logdrain_custom_config_parser' => 'nullable',
]);
$this->server->settings->update([
'is_logdrain_newrelic_enabled' => false,
'is_logdrain_highlight_enabled' => false,
'is_logdrain_axiom_enabled' => false,
]);
}
$this->server->settings->save();
@@ -126,6 +141,10 @@ class LogDrains extends Component
$this->server->settings->update([
'is_logdrain_axiom_enabled' => false,
]);
} else if ($type === 'custom') {
$this->server->settings->update([
'is_logdrain_custom_enabled' => false,
]);
}
handleError($e, $this);
return false;

View File

@@ -21,7 +21,7 @@ class ByIp extends Component
public string $ip;
public string $user = 'root';
public int $port = 22;
public bool $is_part_of_swarm = false;
public bool $is_swarm_manager = false;
protected $rules = [
'name' => 'required|string',
@@ -29,6 +29,7 @@ class ByIp extends Component
'ip' => 'required',
'user' => 'required|string',
'port' => 'required|integer',
'is_swarm_manager' => 'required|boolean',
];
protected $validationAttributes = [
'name' => 'Name',
@@ -36,6 +37,7 @@ class ByIp extends Component
'ip' => 'IP Address/Domain',
'user' => 'User',
'port' => 'Port',
'is_swarm_manager' => 'Swarm Manager',
];
public function mount()
@@ -72,11 +74,11 @@ class ByIp extends Component
'proxy' => [
"type" => ProxyTypes::TRAEFIK_V2->value,
"status" => ProxyStatus::EXITED->value,
]
],
]);
$server->settings->is_part_of_swarm = $this->is_part_of_swarm;
$server->settings->is_swarm_manager = $this->is_swarm_manager;
$server->settings->save();
$server->addInitialNetwork();
return redirect()->route('server.show', $server->uuid);
} catch (\Throwable $e) {
return handleError($e, $this);

View File

@@ -58,11 +58,25 @@ class Deploy extends Component
public function stop()
{
instant_remote_process([
"docker rm -f coolify-proxy",
], $this->server);
$this->server->proxy->status = 'exited';
$this->server->save();
$this->emit('proxyStatusUpdated');
try {
if ($this->server->isSwarm()) {
instant_remote_process([
"docker service rm coolify-proxy_traefik",
], $this->server);
$this->server->proxy->status = 'exited';
$this->server->save();
$this->emit('proxyStatusUpdated');
} else {
instant_remote_process([
"docker rm -f coolify-proxy",
], $this->server);
$this->server->proxy->status = 'exited';
$this->server->save();
$this->emit('proxyStatusUpdated');
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -14,9 +14,12 @@ class Status extends Component
public int $numberOfPolls = 0;
protected $listeners = ['proxyStatusUpdated', 'startProxyPolling'];
public function mount() {
$this->checkProxy();
}
public function startProxyPolling()
{
$this->polling = true;
$this->checkProxy();
}
public function proxyStatusUpdated()
{

View File

@@ -19,13 +19,14 @@ class Show extends Component
if (is_null($this->server)) {
return redirect()->route('server.all');
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function submit()
{
$this->emit('serverRefresh');
$this->emit('serverRefresh',false);
}
public function render()
{

View File

@@ -11,6 +11,9 @@ class PricingPlans extends Component
public bool $isTrial = false;
public function mount() {
$this->isTrial = !data_get(currentTeam(),'subscription.stripe_trial_already_ended');
if (config('constants.limits.trial_period') == 0) {
$this->isTrial = false;
}
}
public function subscribeStripe($type)
{
@@ -63,6 +66,7 @@ class PricingPlans extends Component
];
if (!data_get($team,'subscription.stripe_trial_already_ended')) {
if (config('constants.limits.trial_period') > 0) {
$payload['subscription_data'] = [
'trial_period_days' => config('constants.limits.trial_period'),
'trial_settings' => [
@@ -71,6 +75,7 @@ class PricingPlans extends Component
]
],
];
}
$payload['payment_method_collection'] = 'if_required';
}
$customer = currentTeam()->subscription?->stripe_customer_id ?? null;

View File

@@ -24,6 +24,7 @@ use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use RuntimeException;
use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml;
use Throwable;
@@ -54,6 +55,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private GithubApp|GitlabApp|string $source = 'other';
private StandaloneDocker|SwarmDocker $destination;
private Server $server;
private Server $mainServer;
private ?ApplicationPreview $preview = null;
private ?string $git_type = null;
@@ -71,9 +73,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private $docker_compose;
private $docker_compose_base64;
private string $dockerfile_location = '/Dockerfile';
private string $docker_compose_location = '/docker-compose.yml';
private ?string $addHosts = null;
private ?string $buildTarget = null;
private $log_model;
private Collection $saved_outputs;
private ?string $full_healthcheck_url = null;
@@ -90,9 +92,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
public $tries = 1;
public function __construct(int $application_deployment_queue_id)
{
// ray()->clearScreen();
$this->application_deployment_queue = ApplicationDeploymentQueue::find($application_deployment_queue_id);
$this->log_model = $this->application_deployment_queue;
$this->application = Application::find($this->application_deployment_queue->application_id);
$this->build_pack = data_get($this->application, 'build_pack');
@@ -110,9 +110,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->source = $source->getMorphClass()::where('id', $this->application->source->id)->first();
}
$this->destination = $this->application->destination->getMorphClass()::where('id', $this->application->destination->id)->first();
$this->server = $this->destination->server;
$this->server = $this->mainServer = $this->destination->server;
$this->serverUser = $this->server->user;
$this->basedir = "/artifacts/{$this->deployment_uuid}";
$this->basedir = $this->application->generateBaseDir($this->deployment_uuid);
$this->workdir = "{$this->basedir}" . rtrim($this->application->base_directory, '/');
$this->configuration_dir = application_configuration_dir() . "/{$this->application->uuid}";
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
@@ -156,49 +156,48 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
// Generate custom host<->ip mapping
$allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server);
$allContainers = format_docker_command_output_to_json($allContainers);
$ips = collect([]);
if (count($allContainers) > 0) {
$allContainers = $allContainers[0];
foreach ($allContainers as $container) {
$containerName = data_get($container, 'Name');
if ($containerName === 'coolify-proxy') {
continue;
}
$containerIp = data_get($container, 'IPv4Address');
if ($containerName && $containerIp) {
$containerIp = str($containerIp)->before('/');
$ips->put($containerName, $containerIp->value());
if (!is_null($allContainers)) {
$allContainers = format_docker_command_output_to_json($allContainers);
$ips = collect([]);
if (count($allContainers) > 0) {
$allContainers = $allContainers[0];
foreach ($allContainers as $container) {
$containerName = data_get($container, 'Name');
if ($containerName === 'coolify-proxy') {
continue;
}
$containerIp = data_get($container, 'IPv4Address');
if ($containerName && $containerIp) {
$containerIp = str($containerIp)->before('/');
$ips->put($containerName, $containerIp->value());
}
}
}
$this->addHosts = $ips->map(function ($ip, $name) {
return "--add-host $name:$ip";
})->implode(' ');
}
$this->addHosts = $ips->map(function ($ip, $name) {
return "--add-host $name:$ip";
})->implode(' ');
if ($this->application->dockerfile_target_build) {
$this->buildTarget = " --target {$this->application->dockerfile_target_build} ";
}
// Get user home directory
$this->serverUserHomeDir = instant_remote_process(["echo \$HOME"], $this->server);
$this->dockerConfigFileExists = instant_remote_process(["test -f {$this->serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $this->server);
// Check custom port
preg_match('/(?<=:)\d+(?=\/)/', $this->application->git_repository, $matches);
if (count($matches) === 1) {
$this->customPort = $matches[0];
$gitHost = str($this->application->git_repository)->before(':');
$gitRepo = str($this->application->git_repository)->after('/');
$this->customRepository = "$gitHost:$gitRepo";
} else {
$this->customRepository = $this->application->git_repository;
}
['repository' => $this->customRepository, 'port' => $this->customPort] = $this->application->customRepository();
try {
if ($this->restart_only && $this->application->build_pack !== 'dockerimage') {
$this->just_restart();
if ($this->server->isProxyShouldRun()) {
dispatch(new ContainerStatusJob($this->server));
}
$this->next(ApplicationDeploymentStatus::FINISHED->value);
$this->application->isConfigurationChanged(true);
return;
} else if ($this->application->dockerfile) {
$this->deploy_simple_dockerfile();
} else if ($this->application->build_pack === 'dockercompose') {
$this->deploy_docker_compose_buildpack();
} else if ($this->application->build_pack === 'dockerimage') {
$this->deploy_dockerimage_buildpack();
} else if ($this->application->build_pack === 'dockerfile') {
@@ -215,10 +214,23 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($this->server->isProxyShouldRun()) {
dispatch(new ContainerStatusJob($this->server));
}
if ($this->application->docker_registry_image_name && $this->application->build_pack !== 'dockerimage') {
$this->push_to_docker_registry();
if ($this->server->isSwarm()) {
$this->application_deployment_queue->addLogEntry("Creating / updating stack.");
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "cd {$this->workdir} && docker stack deploy --with-registry-auth -c docker-compose.yml {$this->application->uuid}")
],
[
"echo 'Stack deployed. It may take a few minutes to fully available in your swarm.'"
]
);
}
}
$this->next(ApplicationDeploymentStatus::FINISHED->value);
$this->application->isConfigurationChanged(true);
} catch (Exception $e) {
ray($e);
$this->fail($e);
throw $e;
} finally {
@@ -256,60 +268,70 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
);
}
}
// private function deploy_docker_compose()
// {
// $dockercompose_base64 = base64_encode($this->application->dockercompose);
// $this->execute_remote_command(
// [
// "echo 'Starting deployment of {$this->application->name}.'"
// ],
// );
// $this->prepare_builder_image();
// $this->execute_remote_command(
// [
// executeInDocker($this->deployment_uuid, "echo '$dockercompose_base64' | base64 -d > $this->workdir/docker-compose.yaml")
// ],
// );
// $this->build_image_name = Str::lower("{$this->customRepository}:build");
// $this->production_image_name = Str::lower("{$this->application->uuid}:latest");
// $this->save_environment_variables();
// $containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id);
// ray($containers);
// if ($containers->count() > 0) {
// foreach ($containers as $container) {
// $containerName = data_get($container, 'Names');
// if ($containerName) {
// instant_remote_process(
// ["docker rm -f {$containerName}"],
// $this->application->destination->server
// );
// }
// }
// }
// $this->execute_remote_command(
// ["echo -n 'Starting services (could take a while)...'"],
// [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d"), "hidden" => true],
// );
// }
private function push_to_docker_registry()
{
try {
instant_remote_process(["docker images --format '{{json .}}' {$this->production_image_name}"], $this->server);
$this->execute_remote_command(
[
"echo '\n----------------------------------------'",
],
["echo -n 'Pushing image to docker registry ({$this->production_image_name}).'"],
[
executeInDocker($this->deployment_uuid, "docker push {$this->production_image_name}"), 'hidden' => true
],
);
if ($this->application->docker_registry_image_tag) {
// Tag image with latest
$this->execute_remote_command(
['echo -n "Tagging and pushing image with latest tag."'],
[
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->execute_remote_command([
"echo -n 'Image pushed to docker registry.'"
]);
} catch (Exception $e) {
$this->execute_remote_command(
["echo -n 'Failed to push image to docker registry. Please check debug logs for more information.'"],
);
ray($e);
}
}
private function generate_image_names()
{
if ($this->application->dockerfile) {
$this->build_image_name = Str::lower("{$this->application->uuid}:build");
$this->production_image_name = Str::lower("{$this->application->uuid}:latest");
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) {
$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 {
$tag = Str::of("{$this->commit}-{$this->application->id}-{$this->pull_request_id}");
if (strlen($tag) > 128) {
$tag = $tag->substr(0, 128);
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}");
}
$this->build_image_name = Str::lower("{$this->application->uuid}:{$tag}-build");
$this->production_image_name = Str::lower("{$this->application->uuid}:{$tag}");
}
}
private function just_restart()
@@ -323,23 +345,40 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->check_git_if_build_needed();
$this->set_base_dir();
$this->generate_image_names();
$this->execute_remote_command([
"docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found"
]);
$this->check_image_locally_or_remotely();
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty()) {
$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([
"echo 'Cannot find image {$this->production_image_name} locally. Please redeploy the application.'",
"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([]);
foreach ($this->application->environment_variables as $env) {
$envs->push($env->key . '=' . $env->value);
if ($this->pull_request_id !== 0) {
foreach ($this->application->environment_variables_preview as $env) {
$envs->push($env->key . '=' . $env->value);
}
} else {
foreach ($this->application->environment_variables as $env) {
$envs->push($env->key . '=' . $env->value);
}
}
$envs_base64 = base64_encode($envs->implode("\n"));
$this->execute_remote_command(
@@ -348,6 +387,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
],
);
}
private function deploy_simple_dockerfile()
{
$dockerfile_base64 = base64_encode($this->application->dockerfile);
@@ -385,7 +425,67 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->generate_compose_file();
$this->rolling_update();
}
private function deploy_docker_compose_buildpack()
{
if (data_get($this->application, 'docker_compose_location')) {
$this->docker_compose_location = $this->application->docker_compose_location;
}
if ($this->pull_request_id === 0) {
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->application->name}.");
} else {
$this->application_deployment_queue->addLogEntry("Starting pull request (#{$this->pull_request_id}) deployment of {$this->customRepository}:{$this->application->git_branch}.");
}
$this->server->executeRemoteCommand(
commands: $this->application->prepareHelperImage($this->deployment_uuid),
loggingModel: $this->application_deployment_queue
);
$this->check_git_if_build_needed();
$this->clone_repository();
$this->generate_image_names();
$this->cleanup_git();
$this->application->loadComposeFile(isInit: false);
$composeFile = $this->application->parseCompose(pull_request_id: $this->pull_request_id);
$yaml = Yaml::dump($composeFile->toArray(), 10);
$this->docker_compose_base64 = base64_encode($yaml);
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d > {$this->workdir}{$this->docker_compose_location}"), "hidden" => true
]);
$this->save_environment_variables();
$this->stop_running_container(force: true);
$networkId = $this->application->uuid;
if ($this->pull_request_id !== 0) {
$networkId = "{$this->application->uuid}-{$this->pull_request_id}";
}
if ($this->server->isSwarm()) {
// TODO
} else {
$this->execute_remote_command([
"docker network create --attachable '{$networkId}' >/dev/null || true", "hidden" => true, "ignore_errors" => true
], [
"docker network connect {$networkId} coolify-proxy || true", "hidden" => true, "ignore_errors" => true
]);
}
if (isset($this->docker_compose_base64)) {
$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",
]
);
}
$this->start_by_compose_file();
}
private function deploy_dockerfile_buildpack()
{
if (data_get($this->application, 'dockerfile_location')) {
@@ -406,7 +506,12 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->generate_build_env_variables();
$this->add_build_env_variables_to_dockerfile();
$this->build_image();
// if ($this->application->additional_destinations) {
// $this->push_to_docker_registry();
// $this->deploy_to_additional_destinations();
// } else {
$this->rolling_update();
// }
}
private function deploy_nixpacks_buildpack()
{
@@ -420,12 +525,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->set_base_dir();
$this->generate_image_names();
if (!$this->force_rebuild) {
$this->execute_remote_command([
"docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found"
]);
if (Str::of($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
$this->check_image_locally_or_remotely();
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
$this->create_workdir();
$this->execute_remote_command([
"echo 'No configuration changed & Docker Image found locally with the same Git Commit SHA {$this->application->uuid}:{$this->commit}. Build step skipped.'",
"echo 'No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.'",
]);
$this->generate_compose_file();
$this->rolling_update();
@@ -466,71 +570,83 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function rolling_update()
{
if (count($this->application->ports_mappings_array) > 0) {
$this->execute_remote_command(
["echo -n 'Application has ports mapped to the host system, rolling update is not supported.'"],
);
$this->stop_running_container(force: true);
$this->start_by_compose_file();
if ($this->server->isSwarm()) {
// Skip this.
} else {
$this->execute_remote_command(
["echo -n 'Rolling update started.'"],
);
$this->start_by_compose_file();
$this->health_check();
$this->stop_running_container();
if (count($this->application->ports_mappings_array) > 0) {
$this->execute_remote_command(
[
"echo '\n----------------------------------------'",
],
["echo -n 'Application has ports mapped to the host system, rolling update is not supported.'"],
);
$this->stop_running_container(force: true);
$this->start_by_compose_file();
} else {
$this->execute_remote_command(
[
"echo '\n----------------------------------------'",
],
["echo -n 'Rolling update started.'"],
);
$this->start_by_compose_file();
$this->health_check();
$this->stop_running_container();
$this->application_deployment_queue->addLogEntry("Rolling update completed.");
}
}
}
private function health_check()
{
if ($this->application->isHealthcheckDisabled()) {
$this->newVersionIsHealthy = true;
return;
}
// ray('New container name: ', $this->container_name);
if ($this->container_name) {
$counter = 0;
$this->execute_remote_command(
[
"echo 'Waiting for healthcheck to pass on the new version of your application.'"
]
);
if ($this->full_healthcheck_url) {
if ($this->server->isSwarm()) {
// Implement healthcheck for swarm
} else {
if ($this->application->isHealthcheckDisabled()) {
$this->newVersionIsHealthy = true;
return;
}
// ray('New container name: ', $this->container_name);
if ($this->container_name) {
$counter = 1;
$this->execute_remote_command(
[
"echo 'Healthcheck URL (inside the container): {$this->full_healthcheck_url}'"
"echo 'Waiting for healthcheck to pass on the new container.'"
]
);
}
while ($counter < $this->application->health_check_retries) {
$this->execute_remote_command(
[
"echo 'Attempt {$counter} of {$this->application->health_check_retries}'"
],
[
"docker inspect --format='{{json .State.Health.Status}}' {$this->container_name}",
"hidden" => true,
"save" => "health_check"
],
);
$this->execute_remote_command(
[
"echo 'New version healthcheck status: {$this->saved_outputs->get('health_check')}'"
],
);
if (Str::of($this->saved_outputs->get('health_check'))->contains('healthy')) {
$this->newVersionIsHealthy = true;
if ($this->full_healthcheck_url) {
$this->execute_remote_command(
[
"echo 'Rolling update completed.'"
"echo 'Healthcheck URL (inside the container): {$this->full_healthcheck_url}'"
]
);
}
while ($counter < $this->application->health_check_retries) {
$this->execute_remote_command(
[
"docker inspect --format='{{json .State.Health.Status}}' {$this->container_name}",
"hidden" => true,
"save" => "health_check"
],
);
$this->execute_remote_command(
[
"echo 'Attempt {$counter} of {$this->application->health_check_retries} | Healthcheck status: {$this->saved_outputs->get('health_check')}'"
],
);
$this->application->update(['status' => 'running']);
break;
if (Str::of($this->saved_outputs->get('health_check'))->contains('healthy')) {
$this->newVersionIsHealthy = true;
$this->application->update(['status' => 'running']);
$this->execute_remote_command(
[
"echo 'New container is healthy.'"
],
);
break;
}
$counter++;
sleep($this->application->health_check_interval);
}
$counter++;
sleep($this->application->health_check_interval);
}
}
}
@@ -550,8 +666,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
}
$this->generate_compose_file();
// Needs separate preview variables
// $this->generate_build_env_variables();
// $this->add_build_env_variables_to_dockerfile();
$this->generate_build_env_variables();
$this->add_build_env_variables_to_dockerfile();
$this->build_image();
$this->stop_running_container();
$this->execute_remote_command(
@@ -559,16 +675,26 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d"), "hidden" => true],
);
}
private function create_workdir()
{
$this->execute_remote_command(
[
"command" => executeInDocker($this->deployment_uuid, "mkdir -p {$this->workdir}")
],
);
}
private function prepare_builder_image()
{
$helperImage = config('coolify.helper_image');
if ($this->dockerConfigFileExists === 'OK') {
$runCommand = "docker run -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
} else {
$runCommand = "docker run -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
}
// Get user home directory
$this->serverUserHomeDir = instant_remote_process(["echo \$HOME"], $this->server);
$this->dockerConfigFileExists = instant_remote_process(["test -f {$this->serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $this->server);
if ($this->dockerConfigFileExists === 'OK') {
$runCommand = "docker run -d --network {$this->destination->network} --name {$this->deployment_uuid} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
} else {
$runCommand = "docker run -d --network {$this->destination->network} --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
}
$this->execute_remote_command(
[
"echo -n 'Preparing container with helper image: $helperImage.'",
@@ -580,9 +706,34 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
[
"command" => executeInDocker($this->deployment_uuid, "mkdir -p {$this->basedir}")
],
);
}
private function deploy_to_additional_destinations()
{
$destination_ids = collect(str($this->application->additional_destinations)->explode(','));
foreach ($destination_ids as $destination_id) {
$destination = StandaloneDocker::find($destination_id);
$server = $destination->server;
if ($server->team_id !== $this->mainServer->team_id) {
$this->execute_remote_command(
[
"echo -n 'Skipping deployment to {$server->name}. Not in the same team?!'",
],
);
continue;
}
$this->server = $server;
$this->execute_remote_command(
[
"echo -n 'Deploying to {$this->server->name}.'",
],
);
$this->prepare_builder_image();
$this->generate_image_names();
$this->rolling_update();
}
}
private function set_base_dir()
{
$this->execute_remote_command(
@@ -630,10 +781,12 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function clone_repository()
{
$importCommands = $this->generate_git_import_commands();
$this->application_deployment_queue->addLogEntry("\n----------------------------------------");
$this->application_deployment_queue->addLogEntry("Importing {$this->customRepository}:{$this->application->git_branch} (commit sha {$this->application->git_commit_sha}) to {$this->basedir}.");
if ($this->pull_request_id !== 0) {
$this->application_deployment_queue->addLogEntry("Checking out tag pull/{$this->pull_request_id}/head.");
}
$this->execute_remote_command(
[
"echo -n 'Importing {$this->customRepository}:{$this->application->git_branch} (commit sha {$this->application->git_commit_sha}) to {$this->basedir}. '"
],
[
$importCommands, "hidden" => true
]
@@ -642,90 +795,13 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function generate_git_import_commands()
{
$this->branch = $this->application->git_branch;
$commands = collect([]);
$git_clone_command = "git clone -q -b {$this->application->git_branch}";
if ($this->pull_request_id !== 0) {
$pr_branch_name = "pr-{$this->pull_request_id}-coolify";
}
if ($this->application->deploymentType() === 'source') {
$source_html_url = data_get($this->application, 'source.html_url');
$url = parse_url(filter_var($source_html_url, FILTER_SANITIZE_URL));
$source_html_url_host = $url['host'];
$source_html_url_scheme = $url['scheme'];
if ($this->source->getMorphClass() == 'App\Models\GithubApp') {
if ($this->source->is_public) {
$this->fullRepoUrl = "{$this->source->html_url}/{$this->customRepository}";
$git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$this->customRepository} {$this->basedir}";
$git_clone_command = $this->set_git_import_settings($git_clone_command);
$commands->push(executeInDocker($this->deployment_uuid, $git_clone_command));
} else {
$github_access_token = generate_github_installation_token($this->source);
$commands->push(executeInDocker($this->deployment_uuid, "git clone -q -b {$this->application->git_branch} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$this->customRepository}.git {$this->basedir}"));
$this->fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$this->customRepository}.git";
}
if ($this->pull_request_id !== 0) {
$this->branch = "pull/{$this->pull_request_id}/head:$pr_branch_name";
$commands->push(executeInDocker($this->deployment_uuid, "cd {$this->basedir} && git fetch origin $this->branch && git checkout $pr_branch_name"));
}
return $commands->implode(' && ');
}
}
if ($this->application->deploymentType() === 'deploy_key') {
$this->fullRepoUrl = $this->customRepository;
$private_key = data_get($this->application, 'private_key.private_key');
if (is_null($private_key)) {
throw new Exception('Private key not found. Please add a private key to the application and try again.');
}
$private_key = base64_encode($private_key);
$git_clone_command_base = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$this->customRepository} {$this->basedir}";
$git_clone_command = $this->set_git_import_settings($git_clone_command_base);
$commands = collect([
executeInDocker($this->deployment_uuid, "mkdir -p /root/.ssh"),
executeInDocker($this->deployment_uuid, "echo '{$private_key}' | base64 -d > /root/.ssh/id_rsa"),
executeInDocker($this->deployment_uuid, "chmod 600 /root/.ssh/id_rsa"),
]);
if ($this->pull_request_id !== 0) {
ray($this->git_type);
if ($this->git_type === 'gitlab') {
$this->branch = "merge-requests/{$this->pull_request_id}/head:$pr_branch_name";
$commands->push(executeInDocker($this->deployment_uuid, "echo 'Checking out $this->branch'"));
$git_clone_command = "{$git_clone_command} && cd {$this->basedir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $this->branch && git checkout $pr_branch_name";
}
if ($this->git_type === 'github') {
$this->branch = "pull/{$this->pull_request_id}/head:$pr_branch_name";
$commands->push(executeInDocker($this->deployment_uuid, "echo 'Checking out $this->branch'"));
$git_clone_command = "{$git_clone_command} && cd {$this->basedir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $this->branch && git checkout $pr_branch_name";
}
}
$commands->push(executeInDocker($this->deployment_uuid, $git_clone_command));
return $commands->implode(' && ');
}
if ($this->application->deploymentType() === 'other') {
$this->fullRepoUrl = $this->customRepository;
$git_clone_command = "{$git_clone_command} {$this->customRepository} {$this->basedir}";
$git_clone_command = $this->set_git_import_settings($git_clone_command);
$commands->push(executeInDocker($this->deployment_uuid, $git_clone_command));
return $commands->implode(' && ');
}
['commands' => $commands, 'branch' => $this->branch, 'fullRepoUrl' => $this->fullRepoUrl] = $this->application->generateGitImportCommands($this->deployment_uuid, $this->pull_request_id, $this->git_type);
return $commands;
}
private function set_git_import_settings($git_clone_command)
{
if ($this->application->git_commit_sha !== 'HEAD') {
$git_clone_command = "{$git_clone_command} && cd {$this->basedir} && git -c advice.detachedHead=false checkout {$this->application->git_commit_sha} >/dev/null 2>&1";
}
if ($this->application->settings->is_git_submodules_enabled) {
$git_clone_command = "{$git_clone_command} && cd {$this->basedir} && git submodule update --init --recursive";
}
if ($this->application->settings->is_git_lfs_enabled) {
$git_clone_command = "{$git_clone_command} && cd {$this->basedir} && git lfs pull";
}
return $git_clone_command;
return $this->application->setGitImportSettings($this->deployment_uuid, $git_clone_command);
}
private function cleanup_git()
@@ -737,16 +813,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function generate_nixpacks_confs()
{
$this->execute_remote_command(
[
"echo -n 'Generating nixpacks configuration.'",
]
);
$nixpacks_command = $this->nixpacks_build_cmd();
$this->execute_remote_command(
[
"echo -n Running: $nixpacks_command",
"echo -n 'Generating nixpacks configuration with: $nixpacks_command'",
],
[executeInDocker($this->deployment_uuid, $nixpacks_command)],
[executeInDocker($this->deployment_uuid, "cp {$this->workdir}/.nixpacks/Dockerfile {$this->workdir}/Dockerfile")],
@@ -757,7 +827,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function nixpacks_build_cmd()
{
$this->generate_env_variables();
$nixpacks_command = "nixpacks build --cache-key '{$this->application->uuid}' -o {$this->workdir} {$this->env_args} --no-error-without-start";
$cacheKey = $this->application->uuid;
if ($this->pull_request_id !== 0) {
$cacheKey = "{$this->application->uuid}-pr-{$this->pull_request_id}";
}
$nixpacks_command = "nixpacks build --cache-key '{$cacheKey}' -o {$this->workdir} {$this->env_args} --no-error-without-start";
if ($this->application->build_command) {
$nixpacks_command .= " --build-cmd \"{$this->application->build_command}\"";
}
@@ -807,21 +881,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
}
if ($this->pull_request_id !== 0) {
$labels = collect(generateLabelsApplication($this->application, $this->preview));
// $newHostLabel = $newLabels->filter(function ($label) {
// return str($label)->contains('Host');
// });
// $labels = $labels->reject(function ($label) {
// return str($label)->contains('Host');
// });
// ray($labels,$newLabels);
// $labels = $labels->map(function ($label) {
// $pattern = '/([a-zA-Z0-9]+)-(\d+)-(http|https)/';
// $replacement = "$1-pr-{$this->pull_request_id}-$2-$3";
// $newLabel = preg_replace($pattern, $replacement, $label);
// return $newLabel;
// });
// $labels = $labels->merge($newHostLabel);
}
$labels = $labels->merge(defaultLabels($this->application->id, $this->application->uuid, $this->pull_request_id))->toArray();
$docker_compose = [
@@ -832,7 +891,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
'container_name' => $this->container_name,
'restart' => RESTART_MODE,
'environment' => $environment_variables,
'labels' => $labels,
'expose' => $ports,
'networks' => [
$this->destination->network,
@@ -864,7 +922,49 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
]
]
];
if ($this->server->isDrainLogActivated()) {
if ($this->server->isSwarm()) {
data_forget($docker_compose, 'services.' . $this->container_name . '.container_name');
data_forget($docker_compose, 'services.' . $this->container_name . '.expose');
data_forget($docker_compose, 'services.' . $this->container_name . '.restart');
data_forget($docker_compose, 'services.' . $this->container_name . '.mem_limit');
data_forget($docker_compose, 'services.' . $this->container_name . '.memswap_limit');
data_forget($docker_compose, 'services.' . $this->container_name . '.mem_swappiness');
data_forget($docker_compose, 'services.' . $this->container_name . '.mem_reservation');
data_forget($docker_compose, 'services.' . $this->container_name . '.cpus');
data_forget($docker_compose, 'services.' . $this->container_name . '.cpuset');
data_forget($docker_compose, 'services.' . $this->container_name . '.cpu_shares');
$docker_compose['services'][$this->container_name]['deploy'] = [
'placement' => [
'constraints' => [
'node.role == worker'
]
],
'mode' => 'replicated',
'replicas' => 1,
'update_config' => [
'order' => 'start-first'
],
'rollback_config' => [
'order' => 'start-first'
],
'labels' => $labels,
'resources' => [
'limits' => [
'cpus' => $this->application->limits_cpus,
'memory' => $this->application->limits_memory,
],
'reservations' => [
'cpus' => $this->application->limits_cpus,
'memory' => $this->application->limits_memory,
]
]
];
} else {
$docker_compose['services'][$this->container_name]['labels'] = $labels;
}
if ($this->server->isLogDrainEnabled() && $this->application->isLogDrainEnabled()) {
$docker_compose['services'][$this->container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
@@ -874,6 +974,25 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
]
];
}
if ($this->application->settings->is_gpu_enabled) {
$docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'] = [
[
'driver' => data_get($this->application, 'settings.gpu_driver', 'nvidia'),
'capabilities' => ['gpu'],
'options' => data_get($this->application, 'settings.gpu_options', [])
]
];
if (data_get($this->application, 'settings.gpu_count')) {
$count = data_get($this->application, 'settings.gpu_count');
if ($count === 'all') {
$docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'][0]['count'] = $count;
} else {
$docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'][0]['count'] = (int) $count;
}
} else if (data_get($this->application, 'settings.gpu_device_ids')) {
$docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'][0]['ids'] = data_get($this->application, 'settings.gpu_device_ids');
}
}
if ($this->application->isHealthcheckDisabled()) {
data_forget($docker_compose, 'services.' . $this->container_name . '.healthcheck');
}
@@ -892,6 +1011,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
// 'dockerfile' => $this->workdir . $this->dockerfile_location,
// ];
// }
$docker_compose['services'][$this->application->uuid] = $docker_compose['services'][$this->container_name];
data_forget($docker_compose, 'services.' . $this->container_name);
$this->docker_compose = Yaml::dump($docker_compose, 10);
$this->docker_compose_base64 = base64_encode($this->docker_compose);
$this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d > {$this->workdir}/docker-compose.yml"), "hidden" => true]);
@@ -1000,9 +1124,12 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
"echo -n 'Static deployment. Copying static assets to the image.'",
]);
} else {
$this->execute_remote_command([
"echo -n 'Building docker image for your application. To check the current progress, click on Show Debug Logs.'",
]);
$this->execute_remote_command(
[
"echo -n 'Building docker image started.'",
],
["echo -n 'To check the current progress, click on Show Debug Logs.'"]
);
}
if ($this->application->settings->is_static || $this->application->build_pack === 'static') {
@@ -1022,10 +1149,6 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
listen [::]:80;
server_name localhost;
// real_ip_header X-Forwarded-For;
// proxy_set_header X-Real-IP \$remote_addr;
// proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
location / {
root /usr/share/nginx/html;
index index.html;
@@ -1078,23 +1201,27 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
);
} else {
// Pure Dockerfile based deployment
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "docker build --pull $this->buildTarget $this->addHosts --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
]);
if ($this->application->dockerfile) {
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "docker build --pull $this->buildTarget $this->addHosts --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
]);
} else {
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "docker build $this->buildTarget $this->addHosts --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
]);
}
}
$this->execute_remote_command([
"echo -n 'Building docker image completed.'",
]);
}
private function stop_running_container(bool $force = false)
{
$this->execute_remote_command(["echo -n 'Removing old version of your application.'"]);
$this->application_deployment_queue->addLogEntry("Removing old containers.");
if ($this->newVersionIsHealthy || $force) {
$containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id);
if ($this->pull_request_id !== 0) {
$containers = $containers->filter(function ($container) {
return data_get($container, 'Names') === $this->container_name;
});
} else {
if ($this->pull_request_id === 0) {
$containers = $containers->filter(function ($container) {
return data_get($container, 'Names') !== $this->container_name;
});
@@ -1106,8 +1233,8 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
);
});
} else {
$this->application_deployment_queue->addLogEntry("New container is not healthy, rolling back to the old container.");
$this->execute_remote_command(
["echo -n 'New version is not healthy, rolling back to the old version.'"],
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true],
);
}
@@ -1116,18 +1243,17 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
private function start_by_compose_file()
{
if ($this->application->build_pack === 'dockerimage') {
$this->application_deployment_queue->addLogEntry("Pulling latest images from the registry.");
$this->execute_remote_command(
["echo -n 'Pulling latest images from the registry.'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} pull"), "hidden" => true],
["echo -n 'Starting application (could take a while).'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true],
);
} else {
$this->execute_remote_command(
["echo -n 'Starting application (could take a while).'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true],
);
}
$this->application_deployment_queue->addLogEntry("New container started.");
}
private function generate_build_env_variables()
@@ -1152,9 +1278,14 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), "hidden" => true, "save" => 'dockerfile'
]);
$dockerfile = collect(Str::of($this->saved_outputs->get('dockerfile'))->trim()->explode("\n"));
foreach ($this->application->build_environment_variables as $env) {
$dockerfile->splice(1, 0, "ARG {$env->key}={$env->value}");
if ($this->pull_request_id === 0) {
foreach ($this->application->build_environment_variables as $env) {
$dockerfile->splice(1, 0, "ARG {$env->key}={$env->value}");
}
} else {
foreach ($this->application->build_environment_variables_preview as $env) {
$dockerfile->splice(1, 0, "ARG {$env->key}={$env->value}");
}
}
$dockerfile_base64 = base64_encode($dockerfile->implode("\n"));
$this->execute_remote_command([
@@ -1183,11 +1314,15 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
public function failed(Throwable $exception): void
{
$this->execute_remote_command(
["echo 'Oops something is not okay, are you okay? 😢'"],
["echo '{$exception->getMessage()}'"],
["echo -n 'Deployment failed. Removing the new version of your application.'"],
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true]
["echo 'Oops something is not okay, are you okay? 😢'", 'type' => 'err'],
["echo '{$exception->getMessage()}'", 'type' => 'err'],
);
if ($this->application->build_pack !== 'dockercompose') {
$this->execute_remote_command(
["echo -n 'Deployment failed. Removing the new version of your application.'", 'type' => 'err'],
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true]
);
}
$this->next(ApplicationDeploymentStatus::FAILED->value);
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Jobs;
use App\Traits\ExecuteRemoteCommand;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ApplicationRestartJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, ExecuteRemoteCommand;
public $timeout = 3600;
public $tries = 1;
public string $applicationDeploymentQueueId;
public function __construct(string $applicationDeploymentQueueId)
{
$this->applicationDeploymentQueueId = $applicationDeploymentQueueId;
}
public function handle() {
ray('Restarting application');
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace App\Jobs;
use App\Actions\Server\InstallLogDrain;
use App\Models\Server;
use App\Notifications\Container\ContainerRestarted;
use App\Notifications\Container\ContainerStopped;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Sleep;
class CheckLogDrainContainerJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public Server $server)
{
}
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->id))->dontRelease()];
}
public function uniqueId(): int
{
return $this->server->id;
}
public function healthcheck()
{
$status = instant_remote_process(["docker inspect --format='{{json .State.Status}}' coolify-log-drain"], $this->server, false);
if (str($status)->contains('running')) {
return true;
} else {
return false;
}
}
public function handle(): void
{
// ray("checking log drain statuses for {$this->server->id}");
try {
if (!$this->server->isServerReady()) {
return;
};
$containers = instant_remote_process(["docker container ls -q"], $this->server, false);
if (!$containers) {
return;
}
$containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server);
$containers = format_docker_command_output_to_json($containers);
$foundLogDrainContainer = $containers->filter(function ($value, $key) {
return data_get($value, 'Name') === '/coolify-log-drain';
})->first();
if (!$foundLogDrainContainer || !$this->healthcheck()) {
ray('Log drain container not found or unhealthy. Restarting...');
InstallLogDrain::run($this->server);
Sleep::for(10)->seconds();
if ($this->healthcheck()) {
if ($this->server->log_drain_notification_sent) {
$this->server->team->notify(new ContainerRestarted('Coolify Log Drainer', $this->server));
$this->server->update(['log_drain_notification_sent' => false]);
}
return;
}
if (!$this->server->log_drain_notification_sent) {
ray('Log drain container still unhealthy. Sending notification...');
$this->server->team->notify(new ContainerStopped('Coolify Log Drainer', $this->server, null));
$this->server->update(['log_drain_notification_sent' => true]);
}
} else {
if ($this->server->log_drain_notification_sent) {
$this->server->team->notify(new ContainerRestarted('Coolify Log Drainer', $this->server));
$this->server->update(['log_drain_notification_sent' => false]);
}
}
} catch (\Throwable $e) {
send_internal_notification("CheckLogDrainContainerJob failed on ({$this->server->id}) with: " . $e->getMessage());
ray($e->getMessage());
handleError($e);
}
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Jobs;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy;
use App\Models\ApplicationPreview;
@@ -21,10 +22,6 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public Server $server)
{
$this->handle();
}
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->id))->dontRelease()];
@@ -35,54 +32,75 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
return $this->server->id;
}
public function handle(): void
public function __construct(public Server $server)
{
// ray("checking server status for {$this->server->id}");
if (isDev()) $this->handle();
}
public function handle()
{
// ray("checking container statuses for {$this->server->id}");
try {
$this->server->checkServerRediness();
$containers = instant_remote_process(["docker container ls -q"], $this->server);
if (!$containers) {
if (!$this->server->isServerReady()) {
return;
};
if ($this->server->isSwarm()) {
$containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this->server, false);
$containerReplicase = instant_remote_process(["docker service ls --format '{{json .}}'"], $this->server, false);
} else {
// Precheck for containers
$containers = instant_remote_process(["docker container ls -q"], $this->server, false);
if (!$containers) {
return;
}
$containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server, false);
$containerReplicase = null;
}
if (is_null($containers)) {
return;
}
$containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server);
$containers = format_docker_command_output_to_json($containers);
if ($containerReplicase) {
$containerReplicase = format_docker_command_output_to_json($containerReplicase);
foreach ($containerReplicase as $containerReplica) {
$name = data_get($containerReplica, 'Name');
$containers = $containers->map(function ($container) use ($name, $containerReplica) {
if (data_get($container, 'Spec.Name') === $name) {
$replicas = data_get($containerReplica, 'Replicas');
$running = str($replicas)->explode('/')[0];
$total = str($replicas)->explode('/')[1];
if ($running === $total) {
data_set($container, 'State.Status', 'running');
data_set($container, 'State.Health.Status', 'healthy');
} else {
data_set($container, 'State.Status', 'starting');
data_set($container, 'State.Health.Status', 'unhealthy');
}
}
return $container;
});
}
}
$applications = $this->server->applications();
$databases = $this->server->databases();
$services = $this->server->services()->get();
$previews = $this->server->previews();
$this->server->proxyType();
/// Check if proxy is running
$foundProxyContainer = $containers->filter(function ($value, $key) {
return data_get($value, 'Name') === '/coolify-proxy';
})->first();
if (!$foundProxyContainer) {
try {
$shouldStart = CheckProxy::run($this->server);
if ($shouldStart) {
StartProxy::run($this->server, false);
$this->server->team->notify(new ContainerRestarted('coolify-proxy', $this->server));
} else {
ray('Proxy could not be started.');
}
} catch (\Throwable $e) {
ray($e);
}
} else {
$this->server->proxy->status = data_get($foundProxyContainer, 'State.Status');
$this->server->save();
$connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
}
$foundApplications = [];
$foundApplicationPreviews = [];
$foundDatabases = [];
$foundServices = [];
foreach ($containers as $container) {
if ($this->server->isSwarm()) {
$labels = data_get($container, 'Spec.Labels');
$uuid = data_get($labels, 'coolify.name');
} else {
$labels = data_get($container, 'Config.Labels');
}
$containerStatus = data_get($container, 'State.Status');
$containerHealth = data_get($container, 'State.Health.Status', 'unhealthy');
$containerStatus = "$containerStatus ($containerHealth)";
$labels = data_get($container, 'Config.Labels');
$labels = Arr::undot(format_docker_labels_to_json($labels));
$applicationId = data_get($labels, 'coolify.applicationId');
if ($applicationId) {
@@ -118,11 +136,25 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
if ($uuid) {
$database = $databases->where('uuid', $uuid)->first();
if ($database) {
$isPublic = data_get($database, 'is_public');
$foundDatabases[] = $database->id;
$statusFromDb = $database->status;
if ($statusFromDb !== $containerStatus) {
$database->update(['status' => $containerStatus]);
}
if ($isPublic) {
$foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) {
if ($this->server->isSwarm()) {
return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
} else {
return data_get($value, 'Name') === "/$uuid-proxy";
}
})->first();
if (!$foundTcpProxy) {
StartDatabaseProxy::run($database);
$this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server));
}
}
} else {
// Notify user that this container should not be there.
}
@@ -187,7 +219,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
} else {
$url = null;
}
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
$this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
$exitedService->update(['status' => 'exited']);
}
@@ -214,7 +246,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
$url = null;
}
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
$this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
}
$notRunningApplicationPreviews = $previews->pluck('id')->diff($foundApplicationPreviews);
foreach ($notRunningApplicationPreviews as $previewId) {
@@ -239,7 +271,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
$url = null;
}
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
$this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
}
$notRunningDatabases = $databases->pluck('id')->diff($foundDatabases);
foreach ($notRunningDatabases as $database) {
@@ -263,12 +295,38 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
} else {
$url = null;
}
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
$this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
}
// Check if proxy is running
$this->server->proxyType();
$foundProxyContainer = $containers->filter(function ($value, $key) {
if ($this->server->isSwarm()) {
return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik';
} else {
return data_get($value, 'Name') === '/coolify-proxy';
}
})->first();
if (!$foundProxyContainer) {
try {
$shouldStart = CheckProxy::run($this->server);
if ($shouldStart) {
StartProxy::run($this->server, false);
$this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server));
}
} catch (\Throwable $e) {
ray($e);
}
} else {
$this->server->proxy->status = data_get($foundProxyContainer, 'State.Status');
$this->server->save();
$connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
}
} catch (\Throwable $e) {
send_internal_notification('ContainerStatusJob failed with: ' . $e->getMessage());
send_internal_notification("ContainerStatusJob failed on ({$this->server->id}) with: " . $e->getMessage());
ray($e->getMessage());
handleError($e);
return handleError($e);
}
}
}

View File

@@ -3,8 +3,6 @@
namespace App\Jobs;
use App\Models\Server;
use App\Notifications\Server\HighDiskUsage;
use Exception;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
@@ -13,6 +11,7 @@ use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use RuntimeException;
class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted
{
@@ -35,7 +34,7 @@ class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted
}
});
if ($isInprogress) {
throw new Exception('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping...');
throw new RuntimeException('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping...');
}
if (!$this->server->isFunctional()) {
return;

View File

@@ -34,8 +34,9 @@ class ServerStatusJob implements ShouldQueue, ShouldBeEncrypted
{
ray("checking server status for {$this->server->id}");
try {
$this->server->checkServerRediness();
$this->cleanup(notify: false);
if ($this->server->isServerReady()) {
$this->cleanup(notify: false);
}
} catch (\Throwable $e) {
send_internal_notification('ServerStatusJob failed with: ' . $e->getMessage());
ray($e->getMessage());

View File

@@ -6,6 +6,9 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Spatie\Activitylog\Models\Activity;
use Illuminate\Support\Str;
use RuntimeException;
use Symfony\Component\Yaml\Yaml;
use Visus\Cuid2\Cuid2;
class Application extends BaseModel
{
@@ -45,7 +48,17 @@ class Application extends BaseModel
$application->environment_variables_preview()->delete();
});
}
public function link()
{
if (data_get($this, 'environment.project.uuid')) {
return route('project.application.configuration', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'application_uuid' => data_get($this, 'uuid')
]);
}
return null;
}
public function settings()
{
return $this->hasOne(ApplicationSetting::class);
@@ -123,6 +136,36 @@ class Application extends BaseModel
}
);
}
public function dockerComposeLocation(): Attribute
{
return Attribute::make(
set: function ($value) {
if (is_null($value) || $value === '') {
return '/docker-compose.yaml';
} else {
if ($value !== '/') {
return Str::start(Str::replaceEnd('/', '', $value), '/');
}
return Str::start($value, '/');
}
}
);
}
public function dockerComposePrLocation(): Attribute
{
return Attribute::make(
set: function ($value) {
if (is_null($value) || $value === '') {
return '/docker-compose.yaml';
} else {
if ($value !== '/') {
return Str::start(Str::replaceEnd('/', '', $value), '/');
}
return Str::start($value, '/');
}
}
);
}
public function baseDirectory(): Attribute
{
return Attribute::make(
@@ -157,7 +200,16 @@ class Application extends BaseModel
: explode(',', $this->ports_exposes)
);
}
public function serviceType()
{
$found = str(collect(SPECIFIC_SERVICES)->filter(function ($service) {
return str($this->image)->before(':')->value() === $service;
})->first());
if ($found->isNotEmpty()) {
return $found;
}
return null;
}
public function environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', false)->orderBy('key', 'asc');
@@ -224,8 +276,8 @@ class Application extends BaseModel
{
return $this->morphTo();
}
public function isDeploymentInprogress() {
public function isDeploymentInprogress()
{
$deployments = ApplicationDeploymentQueue::where('application_id', $this->id)->where('status', 'in_progress')->count();
if ($deployments > 0) {
return true;
@@ -300,6 +352,10 @@ class Application extends BaseModel
}
return false;
}
public function isLogDrainEnabled()
{
return data_get($this, 'settings.is_log_drain_enabled', false);
}
public function isConfigurationChanged($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->health_check_path . $this->health_check_port . $this->health_check_host . $this->health_check_method . $this->health_check_return_code . $this->health_check_scheme . $this->health_check_response_text . $this->health_check_interval . $this->health_check_timeout . $this->health_check_retries . $this->health_check_start_period . $this->health_check_enabled . $this->limits_memory . $this->limits_swap . $this->limits_swappiness . $this->limits_reservation . $this->limits_cpus . $this->limits_cpuset . $this->limits_cpu_shares . $this->dockerfile . $this->dockerfile_location . $this->custom_labels;
@@ -327,4 +383,299 @@ class Application extends BaseModel
return true;
}
}
public function isMultipleServerDeployment()
{
if (isDev()) {
return true;
}
if (data_get($this, 'additional_destinations') && data_get($this, 'docker_registry_image_name')) {
return true;
}
return false;
}
public function healthCheckUrl()
{
if ($this->dockerfile || $this->build_pack === 'dockerfile' || $this->build_pack === 'dockerimage') {
return null;
}
if (!$this->health_check_port) {
$health_check_port = $this->ports_exposes_array[0];
} else {
$health_check_port = $this->health_check_port;
}
if ($this->health_check_path) {
$full_healthcheck_url = "{$this->health_check_scheme}://{$this->health_check_host}:{$health_check_port}{$this->health_check_path}";
} else {
$full_healthcheck_url = "{$this->health_check_scheme}://{$this->health_check_host}:{$health_check_port}/";
}
return $full_healthcheck_url;
}
function customRepository()
{
preg_match('/(?<=:)\d+(?=\/)/', $this->git_repository, $matches);
$port = 22;
if (count($matches) === 1) {
$port = $matches[0];
$gitHost = str($this->git_repository)->before(':');
$gitRepo = str($this->git_repository)->after('/');
$repository = "$gitHost:$gitRepo";
} else {
$repository = $this->git_repository;
}
return [
'repository' => $repository,
'port' => $port
];
}
function generateBaseDir(string $uuid)
{
return "/artifacts/{$uuid}";
}
function setGitImportSettings(string $deployment_uuid, string $git_clone_command)
{
$baseDir = $this->generateBaseDir($deployment_uuid);
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";
}
if ($this->settings->is_git_submodules_enabled) {
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && git submodule update --init --recursive";
}
if ($this->settings->is_git_lfs_enabled) {
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && git lfs pull";
}
return $git_clone_command;
}
function generateGitImportCommands(string $deployment_uuid, int $pull_request_id = 0, ?string $git_type = null, bool $exec_in_docker = true, bool $only_checkout = false, ?string $custom_base_dir = null)
{
$branch = $this->git_branch;
['repository' => $customRepository, 'port' => $customPort] = $this->customRepository();
$baseDir = $custom_base_dir ?? $this->generateBaseDir($deployment_uuid);
$commands = collect([]);
$git_clone_command = "git clone -b {$this->git_branch}";
if ($only_checkout) {
$git_clone_command = "git clone --no-checkout -b {$this->git_branch}";
}
if ($pull_request_id !== 0) {
$pr_branch_name = "pr-{$pull_request_id}-coolify";
}
if ($this->deploymentType() === 'source') {
$source_html_url = data_get($this, 'source.html_url');
$url = parse_url(filter_var($source_html_url, FILTER_SANITIZE_URL));
$source_html_url_host = $url['host'];
$source_html_url_scheme = $url['scheme'];
if ($this->source->getMorphClass() == 'App\Models\GithubApp') {
if ($this->source->is_public) {
$fullRepoUrl = "{$this->source->html_url}/{$customRepository}";
$git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$customRepository} {$baseDir}";
if (!$only_checkout) {
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command);
}
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, $git_clone_command));
} else {
$commands->push($git_clone_command);
}
} else {
$github_access_token = generate_github_installation_token($this->source);
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "{$git_clone_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git {$baseDir}"));
$fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git";
} else {
$commands->push("{$git_clone_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository} {$baseDir}");
$fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}";
}
}
if ($pull_request_id !== 0) {
$branch = "pull/{$pull_request_id}/head:$pr_branch_name";
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "cd {$baseDir} && git fetch origin {$branch} && git checkout $pr_branch_name"));
} else {
$commands->push("cd {$baseDir} && git fetch origin {$branch} && git checkout $pr_branch_name");
}
}
return [
'commands' => $commands->implode(' && '),
'branch' => $branch,
'fullRepoUrl' => $fullRepoUrl
];
}
}
if ($this->deploymentType() === 'deploy_key') {
$fullRepoUrl = $customRepository;
$private_key = data_get($this, 'private_key.private_key');
if (is_null($private_key)) {
throw new RuntimeException('Private key not found. Please add a private key to the application and try again.');
}
$private_key = base64_encode($private_key);
$git_clone_command_base = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$customRepository} {$baseDir}";
if (!$only_checkout) {
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command_base);
}
if ($exec_in_docker) {
$commands = collect([
executeInDocker($deployment_uuid, "mkdir -p /root/.ssh"),
executeInDocker($deployment_uuid, "echo '{$private_key}' | base64 -d > /root/.ssh/id_rsa"),
executeInDocker($deployment_uuid, "chmod 600 /root/.ssh/id_rsa"),
]);
} else {
$commands = collect([
"mkdir -p /root/.ssh",
"echo '{$private_key}' | base64 -d > /root/.ssh/id_rsa",
"chmod 600 /root/.ssh/id_rsa",
]);
}
if ($pull_request_id !== 0) {
if ($git_type === 'gitlab') {
$branch = "merge-requests/{$pull_request_id}/head:$pr_branch_name";
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
} else {
$commands->push("echo 'Checking out $branch'");
}
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && git checkout $pr_branch_name";
}
if ($git_type === 'github') {
$branch = "pull/{$pull_request_id}/head:$pr_branch_name";
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
} else {
$commands->push("echo 'Checking out $branch'");
}
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && git checkout $pr_branch_name";
}
}
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, $git_clone_command));
} else {
$commands->push($git_clone_command);
}
return [
'commands' => $commands->implode(' && '),
'branch' => $branch,
'fullRepoUrl' => $fullRepoUrl
];
}
if ($this->deploymentType() === 'other') {
$fullRepoUrl = $customRepository;
$git_clone_command = "{$git_clone_command} {$customRepository} {$baseDir}";
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command);
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, $git_clone_command));
} else {
$commands->push($git_clone_command);
}
return [
'commands' => $commands->implode(' && '),
'branch' => $branch,
'fullRepoUrl' => $fullRepoUrl
];
}
}
public function prepareHelperImage(string $deploymentUuid)
{
$basedir = $this->generateBaseDir($deploymentUuid);
$helperImage = config('coolify.helper_image');
$server = data_get($this, 'destination.server');
$network = data_get($this, 'destination.network');
$serverUserHomeDir = instant_remote_process(["echo \$HOME"], $server);
$dockerConfigFileExists = instant_remote_process(["test -f {$serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $server);
$commands = collect([]);
if ($dockerConfigFileExists === 'OK') {
$commands->push([
"command" => "docker run -d --network $network --name $deploymentUuid --rm -v {$serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock $helperImage",
"hidden" => true,
]);
} else {
$commands->push([
"command" => "docker run -d --network {$network} --name {$deploymentUuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}",
"hidden" => true,
]);
}
$commands->push([
"command" => executeInDocker($deploymentUuid, "mkdir -p {$basedir}"),
"hidden" => true,
]);
return $commands;
}
function parseCompose(int $pull_request_id = 0)
{
if ($this->docker_compose_raw) {
$mainCompose = parseDockerComposeFile(resource: $this, isNew: false, pull_request_id: $pull_request_id);
if ($this->getMorphClass() === 'App\Models\Application' && $this->docker_compose_pr_raw) {
parseDockerComposeFile(resource: $this, isNew: false, pull_request_id: $pull_request_id, is_pr: true);
}
return $mainCompose;
} else {
return collect([]);
}
}
function loadComposeFile($isInit = false)
{
$initialDockerComposeLocation = $this->docker_compose_location;
// $initialDockerComposePrLocation = $this->docker_compose_pr_location;
if ($this->build_pack === 'dockercompose') {
if ($isInit && $this->docker_compose_raw) {
return;
}
$uuid = new Cuid2();
['commands' => $cloneCommand] = $this->generateGitImportCommands(deployment_uuid: $uuid, only_checkout: true, exec_in_docker: false, custom_base_dir: '.');
$workdir = rtrim($this->base_directory, '/');
$composeFile = $this->docker_compose_location;
// $prComposeFile = $this->docker_compose_pr_location;
$fileList = collect([".$workdir$composeFile"]);
// if ($composeFile !== $prComposeFile) {
// $fileList->push(".$prComposeFile");
// }
$commands = collect([
"mkdir -p /tmp/{$uuid} && cd /tmp/{$uuid}",
$cloneCommand,
"git sparse-checkout init --cone",
"git sparse-checkout set {$fileList->implode(' ')}",
"git read-tree -mu HEAD",
"cat .$workdir$composeFile",
]);
$composeFileContent = instant_remote_process($commands, $this->destination->server, false);
if (!$composeFileContent) {
$this->docker_compose_location = $initialDockerComposeLocation;
$this->save();
throw new \Exception("Could not load base compose file from $workdir$composeFile");
} else {
$this->docker_compose_raw = $composeFileContent;
$this->save();
}
// if ($composeFile === $prComposeFile) {
// $this->docker_compose_pr_raw = $composeFileContent;
// $this->save();
// } else {
// $commands = collect([
// "cd /tmp/{$uuid}",
// "cat .$workdir$prComposeFile",
// ]);
// $composePrFileContent = instant_remote_process($commands, $this->destination->server, false);
// if (!$composePrFileContent) {
// $this->docker_compose_pr_location = $initialDockerComposePrLocation;
// $this->save();
// throw new \Exception("Could not load compose file from $workdir$prComposeFile");
// } else {
// $this->docker_compose_pr_raw = $composePrFileContent;
// $this->save();
// }
// }
$commands = collect([
"rm -rf /tmp/{$uuid}",
]);
instant_remote_process($commands, $this->destination->server, false);
return [
'parsedServices' => $this->parseCompose(),
'initialDockerComposeLocation' => $this->docker_compose_location,
'initialDockerComposePrLocation' => $this->docker_compose_pr_location,
];
}
}
}

View File

@@ -3,8 +3,48 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;
class ApplicationDeploymentQueue extends Model
{
protected $guarded = [];
public function getOutput($name)
{
if (!$this->logs) {
return null;
}
return collect(json_decode($this->logs))->where('name', $name)->first()?->output ?? null;
}
public function addLogEntry(string $message, string $type = 'stdout', bool $hidden = false)
{
if ($type === 'error') {
$type = 'stderr';
}
$message = str($message)->trim();
if ($message->startsWith('╔')) {
$message = "\n" . $message;
}
$newLogEntry = [
'command' => null,
'output' => remove_iip($message),
'type' => $type,
'timestamp' => Carbon::now('UTC'),
'hidden' => $hidden,
'batch' => 1,
];
if ($this->logs) {
$previousLogs = json_decode($this->logs, associative: true, flags: JSON_THROW_ON_ERROR);
$newLogEntry['order'] = count($previousLogs) + 1;
$previousLogs[] = $newLogEntry;
$this->update([
'logs' => json_encode($previousLogs, flags: JSON_THROW_ON_ERROR),
]);
} else {
$this->update([
'logs' => json_encode([$newLogEntry], flags: JSON_THROW_ON_ERROR),
]);
}
}
}

View File

@@ -5,7 +5,26 @@ namespace App\Models;
class ApplicationPreview extends BaseModel
{
protected $guarded = [];
protected static function booted()
{
static::deleting(function ($preview) {
if ($preview->application->build_pack === 'dockercompose') {
$server = $preview->application->destination->server;
$composeFile = $preview->application->parseCompose(pull_request_id: $preview->pull_request_id);
$volumes = data_get($composeFile, 'volumes');
$networks = data_get($composeFile, 'networks');
$networkKeys = collect($networks)->keys();
$volumeKeys = collect($volumes)->keys();
$volumeKeys->each(function ($key) use ($server) {
instant_remote_process(["docker volume rm -f $key"], $server, false);
});
$networkKeys->each(function ($key) use ($server) {
instant_remote_process(["docker network disconnect $key coolify-proxy"], $server, false);
instant_remote_process(["docker network rm $key"], $server, false);
});
}
});
}
static function findPreviewByApplicationAndPullId(int $application_id, int $pull_request_id)
{
return self::where('application_id', $application_id)->where('pull_request_id', $pull_request_id)->firstOrFail();

View File

@@ -3,8 +3,10 @@
namespace App\Models;
use App\Notifications\Channels\SendsEmail;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;
use Spatie\Url\Url;
class InstanceSettings extends Model implements SendsEmail
{
@@ -16,6 +18,18 @@ class InstanceSettings extends Model implements SendsEmail
'smtp_password' => 'encrypted',
];
public function fqdn(): Attribute
{
return Attribute::make(
set: function ($value) {
if ($value) {
$url = Url::fromString($value);
$host = $url->getHost();
return $url->getScheme() . '://' . $host;
}
}
);
}
public static function get()
{
return InstanceSettings::findOrFail(0);

View File

@@ -2,22 +2,26 @@
namespace App\Models;
use App\Actions\Server\InstallLogDrain;
use App\Actions\Server\InstallNewRelic;
use App\Enums\ApplicationDeploymentStatus;
use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes;
use App\Notifications\Server\Revived;
use App\Notifications\Server\Unreachable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Sleep;
use Spatie\SchemalessAttributes\Casts\SchemalessAttributes;
use Spatie\SchemalessAttributes\SchemalessAttributesTrait;
use Illuminate\Support\Str;
use Illuminate\Support\Stringable;
class Server extends BaseModel
{
use SchemalessAttributesTrait;
public static $batch_counter = 0;
protected static function booted()
{
@@ -31,25 +35,10 @@ class Server extends BaseModel
}
$server->forceFill($payload);
});
static::created(function ($server) {
ServerSetting::create([
'server_id' => $server->id,
]);
if ($server->id === 0) {
StandaloneDocker::create([
'id' => 0,
'name' => 'coolify',
'network' => 'coolify',
'server_id' => $server->id,
]);
} else {
StandaloneDocker::create([
'name' => 'coolify',
'network' => 'coolify',
'server_id' => $server->id,
]);
}
});
static::deleting(function ($server) {
$server->destinations()->each(function ($destination) {
@@ -78,7 +67,7 @@ class Server extends BaseModel
{
$teamId = currentTeam()->id;
$selectArray = collect($select)->concat(['id']);
return Server::whereTeamId($teamId)->with('settings')->select($selectArray->all())->orderBy('name');
return Server::whereTeamId($teamId)->with('settings','swarmDockers','standaloneDockers')->select($selectArray->all())->orderBy('name');
}
static public function isUsable()
@@ -97,7 +86,39 @@ class Server extends BaseModel
{
return $this->hasOne(ServerSetting::class);
}
public function addInitialNetwork() {
if ($this->id === 0) {
if ($this->isSwarm()) {
SwarmDocker::create([
'id' => 0,
'name' => 'coolify',
'network' => 'coolify-overlay',
'server_id' => $this->id,
]);
} else {
StandaloneDocker::create([
'id' => 0,
'name' => 'coolify',
'network' => 'coolify',
'server_id' => $this->id,
]);
}
} else {
if ($this->isSwarm()) {
SwarmDocker::create([
'name' => 'coolify-overlay',
'network' => 'coolify-overlay',
'server_id' => $this->id,
]);
} else {
StandaloneDocker::create([
'name' => 'coolify-overlay',
'network' => 'coolify',
'server_id' => $this->id,
]);
}
}
}
public function proxyType()
{
$proxyType = $this->proxy->get('type');
@@ -120,15 +141,25 @@ class Server extends BaseModel
{
return $this->ip === 'host.docker.internal' || $this->id === 0;
}
public function checkServerRediness()
public function skipServer()
{
if ($this->ip === '1.2.3.4') {
ray('skipping 1.2.3.4');
return true;
}
return false;
}
public function isServerReady()
{
$serverUptimeCheckNumber = $this->unreachable_count;
$serverUptimeCheckNumberMax = 5;
$serverUptimeCheckNumberMax = 3;
$currentTime = now()->timestamp;
$runtime5Minutes = 1 * 60;
// Run for 1 minutes max and check every 5 seconds
while ($currentTime + $runtime5Minutes > now()->timestamp) {
$runtime = 30;
$isReady = false;
// Run for 30 seconds max and check every 5 seconds for 3 times
while ($currentTime + $runtime > now()->timestamp) {
if ($serverUptimeCheckNumber >= $serverUptimeCheckNumberMax) {
if ($this->unreachable_notification_sent === false) {
ray('Server unreachable, sending notification...');
@@ -157,10 +188,11 @@ class Server extends BaseModel
$db->update(['status' => 'exited']);
}
}
throw new \Exception('Server is not reachable.');
$isReady = false;
break;
}
$result = $this->validateConnection();
ray('validateConnection: ' . $result);
// ray('validateConnection: ' . $result);
if (!$result) {
$serverUptimeCheckNumber++;
$this->update([
@@ -169,30 +201,22 @@ class Server extends BaseModel
Sleep::for(5)->seconds();
return;
}
$this->update([
'unreachable_count' => 0,
]);
if (data_get($this, 'unreachable_notification_sent') === true) {
ray('Server is reachable again, sending notification...');
$this->team->notify(new Revived($this));
$this->update(['unreachable_notification_sent' => false]);
}
if (
data_get($this, 'settings.is_reachable') === false ||
data_get($this, 'settings.is_usable') === false
) {
$this->settings()->update([
'is_reachable' => true,
'is_usable' => true
]);
}
$isReady = true;
break;
}
return $isReady;
}
public function getDiskUsage()
{
return instant_remote_process(["df /| tail -1 | awk '{ print $5}' | sed 's/%//g'"], $this, false);
}
public function definedResources()
{
$applications = $this->applications();
$databases = $this->databases();
$services = $this->services();
return $applications->concat($databases)->concat($services->get());
}
public function hasDefinedResources()
{
$applications = $this->applications()->count() > 0;
@@ -221,6 +245,23 @@ class Server extends BaseModel
return $standaloneDocker->applications;
})->flatten();
}
public function dockerComposeBasedApplications()
{
return $this->applications()->filter(function ($application) {
return data_get($application, 'build_pack') === 'dockercompose';
});
}
public function dockerComposeBasedPreviewDeployments()
{
return $this->previews()->filter(function ($preview) {
$applicationId = data_get($preview, 'application_id');
$application = Application::find($applicationId);
if (!$application) {
return false;
}
return data_get($application, 'build_pack') === 'dockercompose';
});
}
public function services()
{
return $this->hasMany(Service::class);
@@ -300,27 +341,69 @@ class Server extends BaseModel
// }
return true;
}
public function logDrain($type)
{
InstallLogDrain::run($this, $type);
}
public function isFunctional()
{
return $this->settings->is_reachable && $this->settings->is_usable;
}
public function isDrainLogActivated() {
return $this->settings->is_logdrain_newrelic_enabled || $this->settings->is_logdrain_highlight_enabled || $this->settings->is_logdrain_axiom_enabled;
public function isLogDrainEnabled()
{
return $this->settings->is_logdrain_newrelic_enabled || $this->settings->is_logdrain_highlight_enabled || $this->settings->is_logdrain_axiom_enabled || $this->settings->is_logdrain_custom_enabled;
}
public function validateOS(): bool | Stringable
{
$os_release = instant_remote_process(['cat /etc/os-release'], $this);
$datas = collect(explode("\n", $os_release));
$collectedData = collect([]);
foreach ($datas as $data) {
$item = Str::of($data)->trim();
$collectedData->put($item->before('=')->value(), $item->after('=')->lower()->replace('"', '')->value());
}
$ID = data_get($collectedData, 'ID');
// $ID_LIKE = data_get($collectedData, 'ID_LIKE');
// $VERSION_ID = data_get($collectedData, 'VERSION_ID');
$supported = collect(SUPPORTED_OS)->filter(function ($supportedOs) use ($ID) {
if (str($supportedOs)->contains($ID)) {
return str($ID);
}
});
if ($supported->count() === 1) {
ray('supported');
return str($supported->first());
} else {
ray('not supported');
return false;
}
}
public function isSwarm()
{
return data_get($this, 'settings.is_swarm_manager') || data_get($this, 'settings.is_swarm_worker');
}
public function validateConnection()
{
$uptime = instant_remote_process(['uptime'], $this, false);
if (!$uptime) {
$this->settings->is_reachable = false;
$this->settings->save();
$server = Server::find($this->id);
if ($this->skipServer()) {
return false;
}
$this->settings->is_reachable = true;
$this->settings->save();
$uptime = instant_remote_process(['uptime'], $this, false);
if (!$uptime) {
$this->settings()->update([
'is_reachable' => false,
]);
return false;
} else {
$this->settings()->update([
'is_reachable' => true,
]);
$server->update([
'unreachable_count' => 0,
]);
}
if (data_get($this, 'unreachable_notification_sent') === true) {
$this->team->notify(new Revived($this));
$server->update(['unreachable_notification_sent' => false]);
}
return true;
}
public function validateDockerEngine($throwError = false)
@@ -330,13 +413,26 @@ class Server extends BaseModel
$this->settings->is_usable = false;
$this->settings->save();
if ($throwError) {
throw new \Exception('Server is not usable.');
throw new \Exception('Server is not usable. Docker Engine is not installed.');
}
return false;
}
$this->settings->is_usable = true;
$this->settings->save();
$this->validateCoolifyNetwork();
$this->validateCoolifyNetwork(isSwarm: false);
return true;
}
public function validateDockerSwarm()
{
$swarmStatus = instant_remote_process(["docker info|grep -i swarm"], $this, false);
$swarmStatus = str($swarmStatus)->trim()->after(':')->trim();
if ($swarmStatus === 'inactive') {
throw new \Exception('Docker Swarm is not initiated. Please join the server to a swarm before continuing.');
return false;
}
$this->settings->is_usable = true;
$this->settings->save();
$this->validateCoolifyNetwork(isSwarm: true);
return true;
}
public function validateDockerEngineVersion()
@@ -348,12 +444,96 @@ class Server extends BaseModel
$this->settings->save();
return false;
}
$this->settings->is_reachable = true;
$this->settings->is_usable = true;
$this->settings->save();
return true;
}
public function validateCoolifyNetwork()
public function validateCoolifyNetwork($isSwarm = false)
{
return instant_remote_process(["docker network create coolify --attachable >/dev/null 2>&1 || true"], $this, false);
if ($isSwarm) {
return instant_remote_process(["docker network create --attachable --driver overlay coolify-overlay >/dev/null 2>&1 || true"], $this, false);
} else {
return instant_remote_process(["docker network create coolify --attachable >/dev/null 2>&1 || true"], $this, false);
}
}
public function executeRemoteCommand(Collection $commands, ?ApplicationDeploymentQueue $loggingModel = null)
{
static::$batch_counter++;
foreach ($commands as $command) {
$realCommand = data_get($command, 'command');
if (is_null($realCommand)) {
throw new \RuntimeException('Command is not set');
}
$hidden = data_get($command, 'hidden', false);
$ignoreErrors = data_get($command, 'ignoreErrors', false);
$customOutputType = data_get($command, 'customOutputType');
$name = data_get($command, 'name');
$remoteCommand = generateSshCommand($this, $realCommand);
$process = Process::timeout(3600)->idleTimeout(3600)->start($remoteCommand, function (string $type, string $output) use ($realCommand, $hidden, $customOutputType, $loggingModel, $name) {
$output = str($output)->trim();
if ($output->startsWith('╔')) {
$output = "\n" . $output;
}
$newLogEntry = [
'command' => remove_iip($realCommand),
'output' => remove_iip($output),
'type' => $customOutputType ?? $type === 'err' ? 'stderr' : 'stdout',
'timestamp' => Carbon::now('UTC'),
'hidden' => $hidden,
'batch' => static::$batch_counter,
];
if ($loggingModel) {
if (!$loggingModel->logs) {
$newLogEntry['order'] = 1;
} else {
$previousLogs = json_decode($loggingModel->logs, associative: true, flags: JSON_THROW_ON_ERROR);
$newLogEntry['order'] = count($previousLogs) + 1;
}
if ($name) {
$newLogEntry['name'] = $name;
}
$previousLogs[] = $newLogEntry;
$loggingModel->logs = json_encode($previousLogs, flags: JSON_THROW_ON_ERROR);
$loggingModel->save();
}
});
if ($loggingModel) {
$loggingModel->update([
'current_process_id' => $process->id(),
]);
}
$processResult = $process->wait();
if ($processResult->exitCode() !== 0) {
if (!$ignoreErrors) {
if ($loggingModel) {
$status = ApplicationDeploymentStatus::FAILED->value;
$loggingModel->status = $status;
$loggingModel->save();
}
throw new \RuntimeException($processResult->errorOutput());
}
}
}
}
public function stopApplicationRelatedRunningContainers(string $applicationId, string $containerName)
{
$containers = getCurrentApplicationContainerStatus($this, $applicationId, 0);
$containers = $containers->filter(function ($container) use ($containerName) {
return data_get($container, 'Names') !== $containerName;
});
$containers->each(function ($container) {
$removableContainer = data_get($container, 'Names');
$this->server->executeRemoteCommand(
commands: collect([
'command' => "docker rm -f $removableContainer >/dev/null 2>&1",
'hidden' => true,
'ignoreErrors' => true
]),
loggingModel: $this->deploymentQueueEntry
);
});
}
}

View File

@@ -52,53 +52,162 @@ class Service extends BaseModel
foreach ($applications as $application) {
$image = str($application->image)->before(':')->value();
switch ($image) {
case str($image)->contains('minio'):
case str($image)?->contains('minio'):
$data = collect([]);
$console_url = $this->environment_variables()->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first();
$s3_api_url = $this->environment_variables()->where('key', 'MINIO_SERVER_URL')->first();
$admin_user = $this->environment_variables()->where('key', 'SERVICE_USER_MINIO')->first();
if (is_null($admin_user)) {
$admin_user = $this->environment_variables()->where('key', 'MINIO_ROOT_USER')->first();
}
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_MINIO')->first();
$fields->put('MinIO', [
'Console URL' => [
'key' => data_get($console_url, 'key'),
'value' => data_get($console_url, 'value'),
'rules' => 'required|url',
],
'S3 API URL' => [
'key' => data_get($s3_api_url, 'key'),
'value' => data_get($s3_api_url, 'value'),
'rules' => 'required|url',
],
'Admin User' => [
'key' => data_get($admin_user, 'key'),
'value' => data_get($admin_user, 'value'),
'rules' => 'required',
],
'Admin Password' => [
'key' => data_get($admin_password, 'key'),
'value' => data_get($admin_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
if (is_null($admin_password)) {
$admin_password = $this->environment_variables()->where('key', 'MINIO_ROOT_PASSWORD')->first();
}
if ($console_url) {
$data = $data->merge([
'Console URL' => [
'key' => data_get($console_url, 'key'),
'value' => data_get($console_url, 'value'),
'rules' => 'required|url',
],
]);
}
if ($s3_api_url) {
$data = $data->merge([
'S3 API URL' => [
'key' => data_get($s3_api_url, 'key'),
'value' => data_get($s3_api_url, 'value'),
'rules' => 'required|url',
],
]);
}
if ($admin_user) {
$data = $data->merge([
'Admin User' => [
'key' => data_get($admin_user, 'key'),
'value' => data_get($admin_user, 'value'),
'rules' => 'required',
],
]);
}
if ($admin_password) {
$data = $data->merge([
'Admin Password' => [
'key' => data_get($admin_password, 'key'),
'value' => data_get($admin_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
}
$fields->put('MinIO', $data->toArray());
break;
case str($image)->contains('weblate'):
case str($image)?->contains('weblate'):
$data = collect([]);
$admin_email = $this->environment_variables()->where('key', 'WEBLATE_ADMIN_EMAIL')->first();
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_WEBLATE')->first();
$fields->put('Weblate', [
'Admin Email' => [
'key' => data_get($admin_email, 'key'),
'value' => data_get($admin_email, 'value'),
'rules' => 'required|email',
],
'Admin Password' => [
'key' => data_get($admin_password, 'key'),
'value' => data_get($admin_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
if ($admin_email) {
$data = $data->merge([
'Admin Email' => [
'key' => data_get($admin_email, 'key'),
'value' => data_get($admin_email, 'value'),
'rules' => 'required|email',
],
]);
}
if ($admin_password) {
$data = $data->merge([
'Admin Password' => [
'key' => data_get($admin_password, 'key'),
'value' => data_get($admin_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
}
$fields->put('Weblate', $data);
break;
case str($image)?->contains('meilisearch'):
$data = collect([]);
$SERVICE_PASSWORD_MEILISEARCH = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_MEILISEARCH')->first();
if ($SERVICE_PASSWORD_MEILISEARCH) {
$data = $data->merge([
'API Key' => [
'key' => data_get($SERVICE_PASSWORD_MEILISEARCH, 'key'),
'value' => data_get($SERVICE_PASSWORD_MEILISEARCH, 'value'),
'isPassword' => true,
],
]);
}
$fields->put('Meilisearch', $data);
break;
case str($image)?->contains('ghost'):
$data = collect([]);
$MAIL_OPTIONS_AUTH_PASS = $this->environment_variables()->where('key', 'MAIL_OPTIONS_AUTH_PASS')->first();
$MAIL_OPTIONS_AUTH_USER = $this->environment_variables()->where('key', 'MAIL_OPTIONS_AUTH_USER')->first();
$MAIL_OPTIONS_SECURE = $this->environment_variables()->where('key', 'MAIL_OPTIONS_SECURE')->first();
$MAIL_OPTIONS_PORT = $this->environment_variables()->where('key', 'MAIL_OPTIONS_PORT')->first();
$MAIL_OPTIONS_SERVICE = $this->environment_variables()->where('key', 'MAIL_OPTIONS_SERVICE')->first();
$MAIL_OPTIONS_HOST = $this->environment_variables()->where('key', 'MAIL_OPTIONS_HOST')->first();
if ($MAIL_OPTIONS_AUTH_PASS) {
$data = $data->merge([
'Mail Password' => [
'key' => data_get($MAIL_OPTIONS_AUTH_PASS, 'key'),
'value' => data_get($MAIL_OPTIONS_AUTH_PASS, 'value'),
'isPassword' => true,
],
]);
}
if ($MAIL_OPTIONS_AUTH_USER) {
$data = $data->merge([
'Mail User' => [
'key' => data_get($MAIL_OPTIONS_AUTH_USER, 'key'),
'value' => data_get($MAIL_OPTIONS_AUTH_USER, 'value'),
],
]);
}
if ($MAIL_OPTIONS_SECURE) {
$data = $data->merge([
'Mail Secure' => [
'key' => data_get($MAIL_OPTIONS_SECURE, 'key'),
'value' => data_get($MAIL_OPTIONS_SECURE, 'value'),
],
]);
}
if ($MAIL_OPTIONS_PORT) {
$data = $data->merge([
'Mail Port' => [
'key' => data_get($MAIL_OPTIONS_PORT, 'key'),
'value' => data_get($MAIL_OPTIONS_PORT, 'value'),
],
]);
}
if ($MAIL_OPTIONS_SERVICE) {
$data = $data->merge([
'Mail Service' => [
'key' => data_get($MAIL_OPTIONS_SERVICE, 'key'),
'value' => data_get($MAIL_OPTIONS_SERVICE, 'value'),
],
]);
}
if ($MAIL_OPTIONS_HOST) {
$data = $data->merge([
'Mail Host' => [
'key' => data_get($MAIL_OPTIONS_HOST, 'key'),
'value' => data_get($MAIL_OPTIONS_HOST, 'value'),
],
]);
}
$fields->put('Ghost', $data);
break;
}
}
ray($fields);
$databases = $this->databases()->get();
foreach ($databases as $database) {
@@ -267,6 +376,17 @@ class Service extends BaseModel
}
}
}
public function link()
{
if (data_get($this, 'environment.project.uuid')) {
return route('project.service.configuration', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'service_uuid' => data_get($this, 'uuid')
]);
}
return null;
}
public function documentation()
{
$services = getServiceTemplates();
@@ -338,508 +458,12 @@ class Service extends BaseModel
public function parse(bool $isNew = false): Collection
{
// ray()->clearAll();
if ($this->docker_compose_raw) {
try {
$yaml = Yaml::parse($this->docker_compose_raw);
} catch (\Exception $e) {
throw new \Exception($e->getMessage());
}
$topLevelVolumes = collect(data_get($yaml, 'volumes', []));
$topLevelNetworks = collect(data_get($yaml, 'networks', []));
$dockerComposeVersion = data_get($yaml, 'version') ?? '3.8';
$services = data_get($yaml, 'services');
$generatedServiceFQDNS = collect([]);
if (is_null($this->destination)) {
$destination = $this->server->destinations()->first();
if ($destination) {
$this->destination()->associate($destination);
$this->save();
}
}
$definedNetwork = collect([$this->uuid]);
$services = collect($services)->map(function ($service, $serviceName) use ($topLevelVolumes, $topLevelNetworks, $definedNetwork, $isNew, $generatedServiceFQDNS) {
$serviceVolumes = collect(data_get($service, 'volumes', []));
$servicePorts = collect(data_get($service, 'ports', []));
$serviceNetworks = collect(data_get($service, 'networks', []));
$serviceVariables = collect(data_get($service, 'environment', []));
$serviceLabels = collect(data_get($service, 'labels', []));
$containerName = "$serviceName-{$this->uuid}";
// Decide if the service is a database
$isDatabase = false;
$image = data_get_str($service, 'image');
if ($image->contains(':')) {
$image = Str::of($image);
} else {
$image = Str::of($image)->append(':latest');
}
$imageName = $image->before(':');
if (collect(DATABASE_DOCKER_IMAGES)->contains($imageName)) {
$isDatabase = true;
}
data_set($service, 'is_database', $isDatabase);
// Create new serviceApplication or serviceDatabase
if ($isDatabase) {
if ($isNew) {
$savedService = ServiceDatabase::create([
'name' => $serviceName,
'image' => $image,
'service_id' => $this->id
]);
} else {
$savedService = ServiceDatabase::where([
'name' => $serviceName,
'service_id' => $this->id
])->first();
}
} else {
if ($isNew) {
$savedService = ServiceApplication::create([
'name' => $serviceName,
'image' => $image,
'service_id' => $this->id
]);
} else {
$savedService = ServiceApplication::where([
'name' => $serviceName,
'service_id' => $this->id
])->first();
}
}
if (is_null($savedService)) {
if ($isDatabase) {
$savedService = ServiceDatabase::create([
'name' => $serviceName,
'image' => $image,
'service_id' => $this->id
]);
} else {
$savedService = ServiceApplication::create([
'name' => $serviceName,
'image' => $image,
'service_id' => $this->id
]);
}
}
// Check if image changed
if ($savedService->image !== $image) {
$savedService->image = $image;
$savedService->save();
}
// Collect/create/update networks
if ($serviceNetworks->count() > 0) {
foreach ($serviceNetworks as $networkName => $networkDetails) {
$networkExists = $topLevelNetworks->contains(function ($value, $key) use ($networkName) {
return $value == $networkName || $key == $networkName;
});
if (!$networkExists) {
$topLevelNetworks->put($networkDetails, null);
}
}
}
// Collect/create/update ports
$collectedPorts = collect([]);
if ($servicePorts->count() > 0) {
foreach ($servicePorts as $sport) {
if (is_string($sport) || is_numeric($sport)) {
$collectedPorts->push($sport);
}
if (is_array($sport)) {
$target = data_get($sport, 'target');
$published = data_get($sport, 'published');
$protocol = data_get($sport, 'protocol');
$collectedPorts->push("$target:$published/$protocol");
}
}
}
$savedService->ports = $collectedPorts->implode(',');
$savedService->save();
// Add Coolify specific networks
$definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) {
return $value == $definedNetwork;
});
if (!$definedNetworkExists) {
foreach ($definedNetwork as $network) {
$topLevelNetworks->put($network, [
'name' => $network,
'external' => true
]);
}
}
$networks = collect();
foreach ($serviceNetworks as $key => $serviceNetwork) {
if (gettype($serviceNetwork) === 'string') {
// networks:
// - appwrite
$networks->put($serviceNetwork, null);
} else if (gettype($serviceNetwork) === 'array') {
// networks:
// default:
// ipv4_address: 192.168.203.254
// $networks->put($serviceNetwork, null);
ray($key);
$networks->put($key, $serviceNetwork);
}
}
foreach ($definedNetwork as $key => $network) {
$networks->put($network, null);
}
data_set($service, 'networks', $networks->toArray());
// Collect/create/update volumes
if ($serviceVolumes->count() > 0) {
$serviceVolumes = $serviceVolumes->map(function ($volume) use ($savedService, $topLevelVolumes) {
$type = null;
$source = null;
$target = null;
$content = null;
$isDirectory = false;
if (is_string($volume)) {
$source = Str::of($volume)->before(':');
$target = Str::of($volume)->after(':')->beforeLast(':');
if ($source->startsWith('./') || $source->startsWith('/') || $source->startsWith('~')) {
$type = Str::of('bind');
} else {
$type = Str::of('volume');
}
} else if (is_array($volume)) {
$type = data_get_str($volume, 'type');
$source = data_get_str($volume, 'source');
$target = data_get_str($volume, 'target');
$content = data_get($volume, 'content');
$isDirectory = (bool) data_get($volume, 'isDirectory', false);
$foundConfig = $savedService->fileStorages()->whereMountPath($target)->first();
if ($foundConfig) {
$contentNotNull = data_get($foundConfig, 'content');
if ($contentNotNull) {
$content = $contentNotNull;
}
$isDirectory = (bool) data_get($foundConfig, 'is_directory');
}
}
if ($type->value() === 'bind') {
if ($source->value() === "/var/run/docker.sock") {
return $volume;
}
if ($source->value() === '/tmp' || $source->value() === '/tmp/') {
return $volume;
}
LocalFileVolume::updateOrCreate(
[
'mount_path' => $target,
'resource_id' => $savedService->id,
'resource_type' => get_class($savedService)
],
[
'fs_path' => $source,
'mount_path' => $target,
'content' => $content,
'is_directory' => $isDirectory,
'resource_id' => $savedService->id,
'resource_type' => get_class($savedService)
]
);
} else if ($type->value() === 'volume') {
$slugWithoutUuid = Str::slug($source, '-');
$name = "{$savedService->service->uuid}_{$slugWithoutUuid}";
if (is_string($volume)) {
$source = Str::of($volume)->before(':');
$target = Str::of($volume)->after(':')->beforeLast(':');
$source = $name;
$volume = "$source:$target";
} else if (is_array($volume)) {
data_set($volume, 'source', $name);
}
$topLevelVolumes->put($name, [
'name' => $name,
]);
LocalPersistentVolume::updateOrCreate(
[
'mount_path' => $target,
'resource_id' => $savedService->id,
'resource_type' => get_class($savedService)
],
[
'name' => $name,
'mount_path' => $target,
'resource_id' => $savedService->id,
'resource_type' => get_class($savedService)
]
);
}
$savedService->getFilesFromServer(isInit: true);
return $volume;
});
data_set($service, 'volumes', $serviceVolumes->toArray());
}
// Add env_file with at least .env to the service
// $envFile = collect(data_get($service, 'env_file', []));
// if ($envFile->count() > 0) {
// if (!$envFile->contains('.env')) {
// $envFile->push('.env');
// }
// } else {
// $envFile = collect(['.env']);
// }
// data_set($service, 'env_file', $envFile->toArray());
// Get variables from the service
foreach ($serviceVariables as $variableName => $variable) {
if (is_numeric($variableName)) {
$variable = Str::of($variable);
if ($variable->contains('=')) {
// - SESSION_SECRET=123
// - SESSION_SECRET=
$key = $variable->before('=');
$value = $variable->after('=');
} else {
// - SESSION_SECRET
$key = $variable;
$value = null;
}
} else {
// SESSION_SECRET: 123
// SESSION_SECRET:
$key = Str::of($variableName);
$value = Str::of($variable);
}
// TODO: here is the problem
if ($key->startsWith('SERVICE_FQDN')) {
if ($isNew || $savedService->fqdn === null) {
$name = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower();
$fqdn = generateFqdn($this->server, "{$name->value()}-{$this->uuid}");
if (substr_count($key->value(), '_') === 3) {
// SERVICE_FQDN_UMAMI_1000
$port = $key->afterLast('_');
} else {
// SERVICE_FQDN_UMAMI
$port = null;
}
if ($port) {
$fqdn = "$fqdn:$port";
}
if (substr_count($key->value(), '_') >= 2) {
if (is_null($value)) {
$value = Str::of('/');
}
$path = $value->value();
if ($generatedServiceFQDNS->count() > 0) {
$alreadyGenerated = $generatedServiceFQDNS->has($key->value());
if ($alreadyGenerated) {
$fqdn = $generatedServiceFQDNS->get($key->value());
} else {
$generatedServiceFQDNS->put($key->value(), $fqdn);
}
} else {
$generatedServiceFQDNS->put($key->value(), $fqdn);
}
$fqdn = "$fqdn$path";
}
if (!$isDatabase) {
if ($savedService->fqdn) {
$fqdn = $savedService->fqdn . ',' . $fqdn;
} else {
$fqdn = $fqdn;
}
$savedService->fqdn = $fqdn;
$savedService->save();
}
}
// data_forget($service, "environment.$variableName");
// $yaml = data_forget($yaml, "services.$serviceName.environment.$variableName");
// if (count(data_get($yaml, 'services.' . $serviceName . '.environment')) === 0) {
// $yaml = data_forget($yaml, "services.$serviceName.environment");
// }
continue;
}
if ($value?->startsWith('$')) {
$value = Str::of(replaceVariables($value));
$key = $value;
$foundEnv = EnvironmentVariable::where([
'key' => $key,
'service_id' => $this->id,
])->first();
if ($value->startsWith('SERVICE_')) {
// Count _ in $value
$count = substr_count($value->value(), '_');
if ($count === 2) {
// SERVICE_FQDN_UMAMI
$command = $value->after('SERVICE_')->beforeLast('_');
$forService = $value->afterLast('_');
$generatedValue = null;
$port = null;
}
if ($count === 3) {
// SERVICE_FQDN_UMAMI_1000
$command = $value->after('SERVICE_')->before('_');
$forService = $value->after('SERVICE_')->after('_')->before('_');
$generatedValue = null;
$port = $value->afterLast('_');
}
if ($command->value() === 'FQDN' || $command->value() === 'URL') {
if (Str::lower($forService) === $serviceName) {
$fqdn = generateFqdn($this->server, $containerName);
} else {
$fqdn = generateFqdn($this->server, Str::lower($forService) . '-' . $this->uuid);
}
if ($port) {
$fqdn = "$fqdn:$port";
}
if ($foundEnv) {
$fqdn = data_get($foundEnv, 'value');
} else {
if ($command->value() === 'URL') {
$fqdn = Str::of($fqdn)->after('://')->value();
}
EnvironmentVariable::create([
'key' => $key,
'value' => $fqdn,
'is_build_time' => false,
'service_id' => $this->id,
'is_preview' => false,
]);
}
if (!$isDatabase) {
if ($command->value() === 'FQDN' && is_null($savedService->fqdn)) {
$savedService->fqdn = $fqdn;
$savedService->save();
}
}
} else {
switch ($command) {
case 'PASSWORD':
$generatedValue = Str::password(symbols: false);
break;
case 'PASSWORD_64':
$generatedValue = Str::password(length: 64, symbols: false);
break;
case 'BASE64_64':
$generatedValue = Str::random(64);
break;
case 'BASE64_128':
$generatedValue = Str::random(128);
break;
case 'BASE64':
$generatedValue = Str::random(32);
break;
case 'USER':
$generatedValue = Str::random(16);
break;
}
if (!$foundEnv) {
EnvironmentVariable::create([
'key' => $key,
'value' => $generatedValue,
'is_build_time' => false,
'service_id' => $this->id,
'is_preview' => false,
]);
}
}
} else {
if ($value->contains(':-')) {
$key = $value->before(':');
$defaultValue = $value->after(':-');
} else if ($value->contains('-')) {
$key = $value->before('-');
$defaultValue = $value->after('-');
} else if ($value->contains(':?')) {
$key = $value->before(':');
$defaultValue = $value->after(':?');
} else if ($value->contains('?')) {
$key = $value->before('?');
$defaultValue = $value->after('?');
} else {
$key = $value;
$defaultValue = null;
}
if ($foundEnv) {
$defaultValue = data_get($foundEnv, 'value');
}
EnvironmentVariable::updateOrCreate([
'key' => $key,
'service_id' => $this->id,
], [
'value' => $defaultValue,
'is_build_time' => false,
'service_id' => $this->id,
'is_preview' => false,
]);
}
}
}
// Add labels to the service
if ($savedService->serviceType()) {
$fqdns = generateServiceSpecificFqdns($savedService, forTraefik: true);
} else {
$fqdns = collect(data_get($savedService, 'fqdns'));
}
$defaultLabels = defaultLabels($this->id, $containerName, type: 'service', subType: $isDatabase ? 'database' : 'application', subId: $savedService->id);
$serviceLabels = $serviceLabels->merge($defaultLabels);
if (!$isDatabase && $fqdns->count() > 0) {
if ($fqdns) {
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($this->uuid, $fqdns, true));
}
}
if ($this->server->isDrainLogActivated()) {
data_set($service, 'logging', [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => "tcp://127.0.0.1:24224",
'fluentd-async' => "true",
'fluentd-sub-second-precision' => "true",
]
]);
}
data_set($service, 'labels', $serviceLabels->toArray());
data_forget($service, 'is_database');
data_set($service, 'restart', RESTART_MODE);
data_set($service, 'container_name', $containerName);
data_forget($service, 'volumes.*.content');
data_forget($service, 'volumes.*.isDirectory');
// Remove unnecessary variables from service.environment
// $withoutServiceEnvs = collect([]);
// collect(data_get($service, 'environment'))->each(function ($value, $key) use ($withoutServiceEnvs) {
// ray($key, $value);
// if (!Str::of($key)->startsWith('$SERVICE_') && !Str::of($value)->startsWith('SERVICE_')) {
// $k = Str::of($value)->before("=");
// $v = Str::of($value)->after("=");
// $withoutServiceEnvs->put($k->value(), $v->value());
// }
// });
// ray($withoutServiceEnvs);
// data_set($service, 'environment', $withoutServiceEnvs->toArray());
return $service;
});
$finalServices = [
'version' => $dockerComposeVersion,
'services' => $services->toArray(),
'volumes' => $topLevelVolumes->toArray(),
'networks' => $topLevelNetworks->toArray(),
];
$this->docker_compose_raw = Yaml::dump($yaml, 10, 2);
$this->docker_compose = Yaml::dump($finalServices, 10, 2);
$this->save();
$this->saveComposeConfigs();
return collect([]);
} else {
return collect([]);
}
return parseDockerComposeFile($this, $isNew);
}
public function networks()
{
$networks = getTopLevelNetworks($this);
// ray($networks);
return $networks;
}
}

View File

@@ -18,6 +18,10 @@ class ServiceApplication extends BaseModel
$service->fileStorages()->delete();
});
}
public function isLogDrainEnabled()
{
return data_get($this, 'is_log_drain_enabled', false);
}
public function type()
{
return 'service';

View File

@@ -16,6 +16,10 @@ class ServiceDatabase extends BaseModel
$service->fileStorages()->delete();
});
}
public function isLogDrainEnabled()
{
return data_get($this, 'is_log_drain_enabled', false);
}
public function type()
{
return 'service';

View File

@@ -41,6 +41,21 @@ class StandaloneMariadb extends BaseModel
$database->environment_variables()->delete();
});
}
public function link()
{
if (data_get($this, 'environment.project.uuid')) {
return route('project.database.configuration', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'database_uuid' => data_get($this, 'uuid')
]);
}
return null;
}
public function isLogDrainEnabled()
{
return data_get($this, 'is_log_drain_enabled', false);
}
public function type(): string
{
return 'standalone-mariadb';

View File

@@ -44,7 +44,21 @@ class StandaloneMongodb extends BaseModel
$database->environment_variables()->delete();
});
}
public function isLogDrainEnabled()
{
return data_get($this, 'is_log_drain_enabled', false);
}
public function link()
{
if (data_get($this, 'environment.project.uuid')) {
return route('project.database.configuration', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'database_uuid' => data_get($this, 'uuid')
]);
}
return null;
}
public function mongoInitdbRootPassword(): Attribute
{
return Attribute::make(

View File

@@ -41,11 +41,27 @@ class StandaloneMysql extends BaseModel
$database->environment_variables()->delete();
});
}
public function link()
{
if (data_get($this, 'environment.project.uuid')) {
return route('project.database.configuration', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'database_uuid' => data_get($this, 'uuid')
]);
}
return null;
}
public function type(): string
{
return 'standalone-mysql';
}
public function isLogDrainEnabled()
{
return data_get($this, 'is_log_drain_enabled', false);
}
public function portsMappings(): Attribute
{
return Attribute::make(

View File

@@ -41,6 +41,21 @@ class StandalonePostgresql extends BaseModel
$database->environment_variables()->delete();
});
}
public function link()
{
if (data_get($this, 'environment.project.uuid')) {
return route('project.database.configuration', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'database_uuid' => data_get($this, 'uuid')
]);
}
return null;
}
public function isLogDrainEnabled()
{
return data_get($this, 'is_log_drain_enabled', false);
}
public function portsMappings(): Attribute
{

View File

@@ -36,6 +36,21 @@ class StandaloneRedis extends BaseModel
$database->environment_variables()->delete();
});
}
public function link()
{
if (data_get($this, 'environment.project.uuid')) {
return route('project.database.configuration', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'database_uuid' => data_get($this, 'uuid')
]);
}
return null;
}
public function isLogDrainEnabled()
{
return data_get($this, 'is_log_drain_enabled', false);
}
public function portsMappings(): Attribute
{

View File

@@ -4,13 +4,57 @@ namespace App\Models;
class SwarmDocker extends BaseModel
{
protected $guarded = [];
public function applications()
{
return $this->morphMany(Application::class, 'destination');
}
public function postgresqls()
{
return $this->morphMany(StandalonePostgresql::class, 'destination');
}
public function redis()
{
return $this->morphMany(StandaloneRedis::class, 'destination');
}
public function mongodbs()
{
return $this->morphMany(StandaloneMongodb::class, 'destination');
}
public function mysqls()
{
return $this->morphMany(StandaloneMysql::class, 'destination');
}
public function mariadbs()
{
return $this->morphMany(StandaloneMariadb::class, 'destination');
}
public function server()
{
return $this->belongsTo(Server::class);
}
public function services()
{
return $this->morphMany(Service::class, 'destination');
}
public function databases()
{
$postgresqls = $this->postgresqls;
$redis = $this->redis;
$mongodbs = $this->mongodbs;
$mysqls = $this->mysqls;
$mariadbs = $this->mariadbs;
return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs);
}
public function attachedTo()
{
return $this->applications?->count() > 0 || $this->databases()->count() > 0;
}
}

View File

@@ -52,7 +52,6 @@ class User extends Authenticatable implements SendsEmail
}
public function createToken(string $name, array $abilities = ['*'], DateTimeInterface $expiresAt = null)
{
ray('asd');
$plainTextToken = sprintf(
'%s%s%s',
config('sanctum.token_prefix', ''),

View File

@@ -27,7 +27,7 @@ class ContainerRestarted extends Notification implements ShouldQueue
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject("Coolify: Container ({$this->name}) has been restarted automatically on {$this->server->name}");
$mail->subject("Coolify: A service ({$this->name}) has been restarted automatically on {$this->server->name}");
$mail->view('emails.container-restarted', [
'containerName' => $this->name,
'serverName' => $this->server->name,
@@ -38,12 +38,12 @@ class ContainerRestarted extends Notification implements ShouldQueue
public function toDiscord(): string
{
$message = "Coolify: Container ({$this->name}) has been restarted automatically on {$this->server->name}";
$message = "Coolify: A service ({$this->name}) has been restarted automatically on {$this->server->name}";
return $message;
}
public function toTelegram(): array
{
$message = "Coolify: Container ({$this->name}) has been restarted automatically on {$this->server->name}";
$message = "Coolify: A service ({$this->name}) has been restarted automatically on {$this->server->name}";
$payload = [
"message" => $message,
];

View File

@@ -26,7 +26,7 @@ class ContainerStopped extends Notification implements ShouldQueue
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject("Coolify: Container ({$this->name}) has been stopped on {$this->server->name}");
$mail->subject("Coolify: A service ({$this->name}) has been stopped on {$this->server->name}");
$mail->view('emails.container-stopped', [
'containerName' => $this->name,
'serverName' => $this->server->name,
@@ -37,12 +37,12 @@ class ContainerStopped extends Notification implements ShouldQueue
public function toDiscord(): string
{
$message = "Coolify: Container ({$this->name}) has been stopped on {$this->server->name}";
$message = "Coolify: A service ({$this->name}) has been stopped on {$this->server->name}";
return $message;
}
public function toTelegram(): array
{
$message = "Coolify: Container ($this->name} has been stopped on {$this->server->name}";
$message = "Coolify: A service ($this->name} has been stopped on {$this->server->name}";
$payload = [
"message" => $message,
];

View File

@@ -43,7 +43,7 @@ class Unreachable extends Notification implements ShouldQueue
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject("Coolify: Server ({$this->server->name}) is unreachable after trying to connect to it 5 times");
$mail->subject("Coolify: Server ({$this->server->name}) is unreachable after trying to connect to it 3 times");
$mail->view('emails.server-lost-connection', [
'name' => $this->server->name,
]);
@@ -52,13 +52,13 @@ class Unreachable extends Notification implements ShouldQueue
public function toDiscord(): string
{
$message = "Coolify: Server '{$this->server->name}' is unreachable after trying to connect to it 5 times. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server. If your server is back online, we will automatically turn on all automations & integrations.";
$message = "Coolify: Server '{$this->server->name}' is unreachable after trying to connect to it 3 times. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server. If your server is back online, we will automatically turn on all automations & integrations.";
return $message;
}
public function toTelegram(): array
{
return [
"message" => "Coolify: Server '{$this->server->name}' is unreachable after trying to connect to it 5 times. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server. If your server is back online, we will automatically turn on all automations & integrations."
"message" => "Coolify: Server '{$this->server->name}' is unreachable after trying to connect to it 3 times. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server. If your server is back online, we will automatically turn on all automations & integrations."
];
}
}

View File

@@ -12,7 +12,7 @@ use Illuminate\Support\Str;
trait ExecuteRemoteCommand
{
public ?string $save = null;
public static int $batch_counter = 0;
public function execute_remote_command(...$commands)
{
static::$batch_counter++;
@@ -24,54 +24,53 @@ trait ExecuteRemoteCommand
if ($this->server instanceof Server === false) {
throw new \RuntimeException('Server is not set or is not an instance of Server model');
}
$commandsText->each(function ($single_command) {
$command = data_get($single_command, 'command') ?? $single_command[0] ?? null;
if ($command === null) {
throw new \RuntimeException('Command is not set');
}
$hidden = data_get($single_command, 'hidden', false);
$customType = data_get($single_command, 'type');
$ignore_errors = data_get($single_command, 'ignore_errors', false);
$this->save = data_get($single_command, 'save');
$remote_command = generateSshCommand($this->server, $command);
$process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden) {
$process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden, $customType) {
$output = Str::of($output)->trim();
if ($output->startsWith('╔')) {
$output = "\n" . $output;
}
$new_log_entry = [
'command' => $command,
'output' => $output,
'type' => $type === 'err' ? 'stderr' : 'stdout',
'command' => remove_iip($command),
'output' => remove_iip($output),
'type' => $customType ?? $type === 'err' ? 'stderr' : 'stdout',
'timestamp' => Carbon::now('UTC'),
'hidden' => $hidden,
'batch' => static::$batch_counter,
];
if (!$this->log_model->logs) {
if (!$this->application_deployment_queue->logs) {
$new_log_entry['order'] = 1;
} else {
$previous_logs = json_decode($this->log_model->logs, associative: true, flags: JSON_THROW_ON_ERROR);
$previous_logs = json_decode($this->application_deployment_queue->logs, associative: true, flags: JSON_THROW_ON_ERROR);
$new_log_entry['order'] = count($previous_logs) + 1;
}
$previous_logs[] = $new_log_entry;
$this->log_model->logs = json_encode($previous_logs, flags: JSON_THROW_ON_ERROR);
$this->log_model->save();
$this->application_deployment_queue->logs = json_encode($previous_logs, flags: JSON_THROW_ON_ERROR);
$this->application_deployment_queue->save();
if ($this->save) {
$this->saved_outputs[$this->save] = Str::of($output)->trim();
}
});
$this->log_model->update([
$this->application_deployment_queue->update([
'current_process_id' => $process->id(),
]);
$process_result = $process->wait();
if ($process_result->exitCode() !== 0) {
if (!$ignore_errors) {
$status = ApplicationDeploymentStatus::FAILED->value;
$this->log_model->status = $status;
$this->log_model->save();
$this->application_deployment_queue->status = ApplicationDeploymentStatus::FAILED->value;
$this->application_deployment_queue->save();
throw new \RuntimeException($process_result->errorOutput());
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace App\Traits;
use App\Enums\ApplicationDeploymentStatus;
use App\Models\Server;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;
trait ExecuteRemoteCommandNew
{
public static $batch_counter = 0;
public function executeRemoteCommand(Server $server, $logModel, $commands)
{
static::$batch_counter++;
if ($commands instanceof Collection) {
$commandsText = $commands;
} else {
$commandsText = collect($commands);
}
$commandsText->each(function ($singleCommand) use ($server, $logModel) {
$command = data_get($singleCommand, 'command') ?? $singleCommand[0] ?? null;
if ($command === null) {
throw new \RuntimeException('Command is not set');
}
$hidden = data_get($singleCommand, 'hidden', false);
$customType = data_get($singleCommand, 'type');
$ignoreErrors = data_get($singleCommand, 'ignore_errors', false);
$save = data_get($singleCommand, 'save');
$remote_command = generateSshCommand($server, $command);
$process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden, $customType, $logModel, $save) {
$output = str($output)->trim();
if ($output->startsWith('╔')) {
$output = "\n" . $output;
}
$newLogEntry = [
'command' => remove_iip($command),
'output' => remove_iip($output),
'type' => $customType ?? $type === 'err' ? 'stderr' : 'stdout',
'timestamp' => Carbon::now('UTC'),
'hidden' => $hidden,
'batch' => static::$batch_counter,
];
if (!$logModel->logs) {
$newLogEntry['order'] = 1;
} else {
$previousLogs = json_decode($logModel->logs, associative: true, flags: JSON_THROW_ON_ERROR);
$newLogEntry['order'] = count($previousLogs) + 1;
}
$previousLogs[] = $newLogEntry;
$logModel->logs = json_encode($previousLogs, flags: JSON_THROW_ON_ERROR);
$logModel->save();
if ($save) {
$this->remoteCommandOutputs[$save] = str($output)->trim();
}
});
$logModel->update([
'current_process_id' => $process->id(),
]);
$processResult = $process->wait();
if ($processResult->exitCode() !== 0) {
if (!$ignoreErrors) {
$status = ApplicationDeploymentStatus::FAILED->value;
$logModel->status = $status;
$logModel->save();
throw new \RuntimeException($processResult->errorOutput());
}
}
});
}
}

View File

@@ -38,7 +38,7 @@ class Textarea extends Component
if (is_null($this->id)) $this->id = new Cuid2(7);
if (is_null($this->name)) $this->name = $this->id;
$this->label = Str::title($this->label);
// $this->label = Str::title($this->label);
return view('components.forms.textarea');
}
}

View File

@@ -1,8 +1,15 @@
<?php
use App\Jobs\ApplicationDeployDockerImageJob;
use App\Jobs\ApplicationDeploymentJob;
use App\Jobs\ApplicationDeploySimpleDockerfileJob;
use App\Jobs\ApplicationRestartJob;
use App\Jobs\MultipleApplicationDeploymentJob;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Models\ApplicationPreview;
use App\Models\Server;
use Symfony\Component\Yaml\Yaml;
function queue_application_deployment(int $application_id, string $deployment_uuid, int | null $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false, ?string $git_type = null)
{
@@ -32,6 +39,7 @@ function queue_application_deployment(int $application_id, string $deployment_uu
dispatch(new ApplicationDeploymentJob(
application_deployment_queue_id: $deployment->id,
))->onConnection('long-running')->onQueue('long-running');
}
function queue_next_deployment(Application $application)
@@ -43,3 +51,262 @@ function queue_next_deployment(Application $application)
))->onConnection('long-running')->onQueue('long-running');
}
}
// Deployment things
function generateHostIpMapping(Server $server, string $network)
{
// Generate custom host<->ip hostnames
$allContainers = instant_remote_process(["docker network inspect {$network} -f '{{json .Containers}}' "], $server);
$allContainers = format_docker_command_output_to_json($allContainers);
$ips = collect([]);
if (count($allContainers) > 0) {
$allContainers = $allContainers[0];
foreach ($allContainers as $container) {
$containerName = data_get($container, 'Name');
if ($containerName === 'coolify-proxy') {
continue;
}
$containerIp = data_get($container, 'IPv4Address');
if ($containerName && $containerIp) {
$containerIp = str($containerIp)->before('/');
$ips->put($containerName, $containerIp->value());
}
}
}
return $ips->map(function ($ip, $name) {
return "--add-host $name:$ip";
})->implode(' ');
}
function generateBaseDir(string $deplyomentUuid)
{
return "/artifacts/$deplyomentUuid";
}
function generateWorkdir(string $deplyomentUuid, Application $application)
{
return generateBaseDir($deplyomentUuid) . rtrim($application->base_directory, '/');
}
function prepareHelperContainer(Server $server, string $network, string $deploymentUuid)
{
$basedir = generateBaseDir($deploymentUuid);
$helperImage = config('coolify.helper_image');
$serverUserHomeDir = instant_remote_process(["echo \$HOME"], $server);
$dockerConfigFileExists = instant_remote_process(["test -f {$serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $server);
$commands = collect([]);
if ($dockerConfigFileExists === 'OK') {
$commands->push([
"command" => "docker run -d --network $network --name $deploymentUuid --rm -v {$serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock $helperImage",
"hidden" => true,
]);
} else {
$commands->push([
"command" => "docker run -d --network {$network} --name {$deploymentUuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}",
"hidden" => true,
]);
}
$commands->push([
"command" => executeInDocker($deploymentUuid, "mkdir -p {$basedir}"),
"hidden" => true,
]);
return $commands;
}
function generateComposeFile(string $deploymentUuid, Server $server, string $network, Application $application, string $containerName, string $imageName, ?ApplicationPreview $preview = null, int $pullRequestId = 0)
{
$ports = $application->settings->is_static ? [80] : $application->ports_exposes_array;
$workDir = generateWorkdir($deploymentUuid, $application);
$persistent_storages = generateLocalPersistentVolumes($application, $pullRequestId);
$volume_names = generateLocalPersistentVolumesOnlyVolumeNames($application, $pullRequestId);
$environment_variables = generateEnvironmentVariables($application, $ports, $pullRequestId);
if (data_get($application, 'custom_labels')) {
$labels = collect(str($application->custom_labels)->explode(','));
$labels = $labels->filter(function ($value, $key) {
return !str($value)->startsWith('coolify.');
});
$application->custom_labels = $labels->implode(',');
$application->save();
} else {
$labels = collect(generateLabelsApplication($application, $preview));
}
if ($pullRequestId !== 0) {
$labels = collect(generateLabelsApplication($application, $preview));
}
$labels = $labels->merge(defaultLabels($application->id, $application->uuid, 0))->toArray();
$docker_compose = [
'version' => '3.8',
'services' => [
$containerName => [
'image' => $imageName,
'container_name' => $containerName,
'restart' => RESTART_MODE,
'environment' => $environment_variables,
'labels' => $labels,
'expose' => $ports,
'networks' => [
$network,
],
'mem_limit' => $application->limits_memory,
'memswap_limit' => $application->limits_memory_swap,
'mem_swappiness' => $application->limits_memory_swappiness,
'mem_reservation' => $application->limits_memory_reservation,
'cpus' => (int) $application->limits_cpus,
'cpuset' => $application->limits_cpuset,
'cpu_shares' => $application->limits_cpu_shares,
]
],
'networks' => [
$network => [
'external' => true,
'name' => $network,
'attachable' => true
]
]
];
if ($server->isLogDrainEnabled() && $application->isLogDrainEnabled()) {
$docker_compose['services'][$containerName]['logging'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => "tcp://127.0.0.1:24224",
'fluentd-async' => "true",
'fluentd-sub-second-precision' => "true",
]
];
}
if ($application->settings->is_gpu_enabled) {
$docker_compose['services'][$containerName]['deploy']['resources']['reservations']['devices'] = [
[
'driver' => data_get($application, 'settings.gpu_driver', 'nvidia'),
'capabilities' => ['gpu'],
'options' => data_get($application, 'settings.gpu_options', [])
]
];
if (data_get($application, 'settings.gpu_count')) {
$count = data_get($application, 'settings.gpu_count');
if ($count === 'all') {
$docker_compose['services'][$containerName]['deploy']['resources']['reservations']['devices'][0]['count'] = $count;
} else {
$docker_compose['services'][$containerName]['deploy']['resources']['reservations']['devices'][0]['count'] = (int) $count;
}
} else if (data_get($application, 'settings.gpu_device_ids')) {
$docker_compose['services'][$containerName]['deploy']['resources']['reservations']['devices'][0]['ids'] = data_get($application, 'settings.gpu_device_ids');
}
}
if ($application->isHealthcheckDisabled()) {
data_forget($docker_compose, 'services.' . $containerName . '.healthcheck');
}
if (count($application->ports_mappings_array) > 0 && $pullRequestId === 0) {
$docker_compose['services'][$containerName]['ports'] = $application->ports_mappings_array;
}
if (count($persistent_storages) > 0) {
$docker_compose['services'][$containerName]['volumes'] = $persistent_storages;
}
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$commands = collect([]);
$commands->push([
"command" => executeInDocker($deploymentUuid, "echo '{$docker_compose_base64}' | base64 -d > {$workDir}/docker-compose.yml"),
"hidden" => true,
]);
return $commands;
}
function generateLocalPersistentVolumes(Application $application, int $pullRequestId = 0)
{
$local_persistent_volumes = [];
foreach ($application->persistentStorages as $persistentStorage) {
$volume_name = $persistentStorage->host_path ?? $persistentStorage->name;
if ($pullRequestId !== 0) {
$volume_name = $volume_name . '-pr-' . $pullRequestId;
}
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
return $local_persistent_volumes;
}
function generateLocalPersistentVolumesOnlyVolumeNames(Application $application, int $pullRequestId = 0)
{
$local_persistent_volumes_names = [];
foreach ($application->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path) {
continue;
}
$name = $persistentStorage->name;
if ($pullRequestId !== 0) {
$name = $name . '-pr-' . $pullRequestId;
}
$local_persistent_volumes_names[$name] = [
'name' => $name,
'external' => false,
];
}
return $local_persistent_volumes_names;
}
function generateEnvironmentVariables(Application $application, $ports, int $pullRequestId = 0)
{
$environment_variables = collect();
// ray('Generate Environment Variables')->green();
if ($pullRequestId === 0) {
// ray($this->application->runtime_environment_variables)->green();
foreach ($application->runtime_environment_variables as $env) {
$environment_variables->push("$env->key=$env->value");
}
foreach ($application->nixpacks_environment_variables as $env) {
$environment_variables->push("$env->key=$env->value");
}
} else {
// ray($this->application->runtime_environment_variables_preview)->green();
foreach ($application->runtime_environment_variables_preview as $env) {
$environment_variables->push("$env->key=$env->value");
}
foreach ($application->nixpacks_environment_variables_preview as $env) {
$environment_variables->push("$env->key=$env->value");
}
}
// Add PORT if not exists, use the first port as default
if ($environment_variables->filter(fn ($env) => str($env)->contains('PORT'))->isEmpty()) {
$environment_variables->push("PORT={$ports[0]}");
}
return $environment_variables->all();
}
function startNewApplication(Application $application, string $deploymentUuid, ApplicationDeploymentQueue $loggingModel)
{
$commands = collect([]);
$workDir = generateWorkdir($deploymentUuid, $application);
if ($application->build_pack === 'dockerimage') {
$loggingModel->addLogEntry('Pulling latest images from the registry.');
$commands->push(
[
"command" => executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} pull"),
"hidden" => true
],
[
"command" => executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} up --build -d"),
"hidden" => true
],
);
} else {
$commands->push(
[
"command" => executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} up --build -d"),
"hidden" => true
],
);
}
return $commands;
}
function removeOldDeployment(string $containerName)
{
$commands = collect([]);
$commands->push(
["docker rm -f $containerName >/dev/null 2>&1"],
);
return $commands;
}

View File

@@ -1,5 +1,6 @@
<?php
const REDACTED = '<REDACTED>';
const DATABASE_TYPES = ['postgresql', 'redis', 'mongodb', 'mysql', 'mariadb'];
const VALID_CRON_STRINGS = [
'every_minute' => '* * * * *',
@@ -26,3 +27,10 @@ const DATABASE_DOCKER_IMAGES = [
const SPECIFIC_SERVICES = [
'quay.io/minio/minio',
];
// Based on /etc/os-release
const SUPPORTED_OS = [
'ubuntu debian raspbian',
'centos fedora rhel ol rocky',
'sles opensuse-leap opensuse-tumbleweed'
];

View File

@@ -24,7 +24,7 @@ function create_standalone_postgresql($environment_id, $destination_uuid): Stand
}
return StandalonePostgresql::create([
'name' => generate_database_name('postgresql'),
'postgres_password' => \Illuminate\Support\Str::password(symbols: false),
'postgres_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
'environment_id' => $environment_id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
@@ -39,7 +39,7 @@ function create_standalone_redis($environment_id, $destination_uuid): Standalone
}
return StandaloneRedis::create([
'name' => generate_database_name('redis'),
'redis_password' => \Illuminate\Support\Str::password(symbols: false),
'redis_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
'environment_id' => $environment_id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
@@ -54,7 +54,7 @@ function create_standalone_mongodb($environment_id, $destination_uuid): Standalo
}
return StandaloneMongodb::create([
'name' => generate_database_name('mongodb'),
'mongo_initdb_root_password' => \Illuminate\Support\Str::password(symbols: false),
'mongo_initdb_root_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
'environment_id' => $environment_id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
@@ -68,8 +68,8 @@ function create_standalone_mysql($environment_id, $destination_uuid): Standalone
}
return StandaloneMysql::create([
'name' => generate_database_name('mysql'),
'mysql_root_password' => \Illuminate\Support\Str::password(symbols: false),
'mysql_password' => \Illuminate\Support\Str::password(symbols: false),
'mysql_root_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
'mysql_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
'environment_id' => $environment_id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
@@ -83,8 +83,8 @@ function create_standalone_mariadb($environment_id, $destination_uuid): Standalo
}
return StandaloneMariadb::create([
'name' => generate_database_name('mariadb'),
'mariadb_root_password' => \Illuminate\Support\Str::password(symbols: false),
'mariadb_password' => \Illuminate\Support\Str::password(symbols: false),
'mariadb_root_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
'mariadb_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
'environment_id' => $environment_id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),

View File

@@ -3,6 +3,9 @@
use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Models\Server;
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Spatie\Url\Url;
@@ -90,7 +93,11 @@ function executeInDocker(string $containerId, string $command)
function getContainerStatus(Server $server, string $container_id, bool $all_data = false, bool $throwError = false)
{
$container = instant_remote_process(["docker inspect --format '{{json .}}' {$container_id}"], $server, $throwError);
if ($server->isSwarm()) {
$container = instant_remote_process(["docker service ls --filter 'name={$container_id}' --format '{{json .}}' "], $server, $throwError);
} else {
$container = instant_remote_process(["docker inspect --format '{{json .}}' {$container_id}"], $server, $throwError);
}
if (!$container) {
return 'exited';
}
@@ -98,7 +105,19 @@ function getContainerStatus(Server $server, string $container_id, bool $all_data
if ($all_data) {
return $container[0];
}
return data_get($container[0], 'State.Status', 'exited');
if ($server->isSwarm()) {
$replicas = data_get($container[0], 'Replicas');
$replicas = explode('/', $replicas);
$active = (int)$replicas[0];
$total = (int)$replicas[1];
if ($active === $total) {
return 'running';
} else {
return 'starting';
}
} else {
return data_get($container[0], 'State.Status', 'exited');
}
}
function generateApplicationContainerName(Application $application, $pull_request_id = 0)
@@ -137,18 +156,28 @@ function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'applica
$labels->push('coolify.name=' . $name);
$labels->push('coolify.pullRequestId=' . $pull_request_id);
if ($type === 'service') {
$labels->push('coolify.service.subId=' . $subId);
$labels->push('coolify.service.subType=' . $subType);
$subId && $labels->push('coolify.service.subId=' . $subId);
$subType && $labels->push('coolify.service.subType=' . $subType);
}
return $labels;
}
function generateServiceSpecificFqdns($service, $forTraefik = false)
function generateServiceSpecificFqdns(ServiceApplication|Application $resource, $forTraefik = false)
{
$variables = collect($service->service->environment_variables);
$type = $service->serviceType();
if ($resource->getMorphClass() === 'App\Models\ServiceApplication') {
$uuid = $resource->uuid;
$server = $resource->service->server;
$environment_variables = $resource->service->environment_variables;
$type = $resource->serviceType();
} else if ($resource->getMorphClass() === 'App\Models\Application') {
$uuid = $resource->uuid;
$server = $resource->destination->server;
$environment_variables = $resource->environment_variables;
$type = $resource->serviceType();
}
$variables = collect($environment_variables);
$payload = collect([]);
switch ($type) {
case $type->contains('minio'):
case $type?->contains('minio'):
$MINIO_BROWSER_REDIRECT_URL = $variables->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first();
$MINIO_SERVER_URL = $variables->where('key', 'MINIO_SERVER_URL')->first();
if (is_null($MINIO_BROWSER_REDIRECT_URL) || is_null($MINIO_SERVER_URL)) {
@@ -156,12 +185,12 @@ function generateServiceSpecificFqdns($service, $forTraefik = false)
}
if (is_null($MINIO_BROWSER_REDIRECT_URL?->value)) {
$MINIO_BROWSER_REDIRECT_URL?->update([
"value" => generateFqdn($service->service->server, 'console-' . $service->uuid)
"value" => generateFqdn($server, 'console-' . $uuid)
]);
}
if (is_null($MINIO_SERVER_URL?->value)) {
$MINIO_SERVER_URL?->update([
"value" => generateFqdn($service->service->server, 'minio-' . $service->uuid)
"value" => generateFqdn($server, 'minio-' . $uuid)
]);
}
if ($forTraefik) {
@@ -175,10 +204,11 @@ function generateServiceSpecificFqdns($service, $forTraefik = false)
$MINIO_SERVER_URL->value,
]);
}
break;
}
return $payload;
}
function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled, $onlyPort = null)
function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null)
{
$labels = collect([]);
$labels->push('traefik.enable=true');
@@ -260,3 +290,21 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
}
return $labels->all();
}
function isDatabaseImage(?string $image = null)
{
if (is_null($image)) {
return false;
}
$image = str($image);
if ($image->contains(':')) {
$image = str($image);
} else {
$image = str($image)->append(':latest');
}
$imageName = $image->before(':');
if (collect(DATABASE_DOCKER_IMAGES)->contains($imageName)) {
return true;
}
return false;
}

View File

@@ -1,6 +1,7 @@
<?php
use App\Actions\Proxy\SaveConfiguration;
use App\Models\Application;
use App\Models\Server;
use Symfony\Component\Yaml\Yaml;
@@ -12,9 +13,32 @@ function get_proxy_path()
}
function connectProxyToNetworks(Server $server)
{
// Standalone networks
$networks = collect($server->standaloneDockers)->map(function ($docker) {
return $docker['network'];
})->unique();
});
// Service networks
foreach ($server->services()->get() as $service) {
$networks->push($service->networks());
}
// Docker compose based apps
$docker_compose_apps = $server->dockerComposeBasedApplications();
foreach ($docker_compose_apps as $app) {
$networks->push($app->uuid);
}
// Docker compose based preview deployments
$docker_compose_previews = $server->dockerComposeBasedPreviewDeployments();
foreach ($docker_compose_previews as $preview) {
$pullRequestId = $preview->pull_request_id;
$applicationId = $preview->application_id;
$application = Application::find($applicationId);
if (!$application) {
continue;
}
$network = "{$application->uuid}-{$pullRequestId}";
$networks->push($network);
}
$networks = collect($networks)->flatten()->unique();
if ($networks->count() === 0) {
$networks = collect(['coolify']);
}
@@ -30,9 +54,15 @@ function connectProxyToNetworks(Server $server)
function generate_default_proxy_configuration(Server $server)
{
$proxy_path = get_proxy_path();
$networks = collect($server->standaloneDockers)->map(function ($docker) {
return $docker['network'];
})->unique();
if ($server->isSwarm()) {
$networks = collect($server->swarmDockers)->map(function ($docker) {
return $docker['network'];
})->unique();
} else {
$networks = collect($server->standaloneDockers)->map(function ($docker) {
return $docker['network'];
})->unique();
}
if ($networks->count() === 0) {
$networks = collect(['coolify']);
}
@@ -42,6 +72,16 @@ function generate_default_proxy_configuration(Server $server)
"external" => true,
];
});
$labels = [
"traefik.enable=true",
"traefik.http.routers.traefik.entrypoints=http",
"traefik.http.routers.traefik.middlewares=traefik-basic-auth@file",
"traefik.http.routers.traefik.service=api@internal",
"traefik.http.services.traefik.loadbalancer.server.port=8080",
// Global Middlewares
"traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https",
"traefik.http.middlewares.gzip.compress=true",
];
$config = [
"version" => "3.8",
"networks" => $array_of_networks->toArray(),
@@ -78,7 +118,6 @@ function generate_default_proxy_configuration(Server $server)
"--entrypoints.https.address=:443",
"--entrypoints.http.http.encodequerysemicolons=true",
"--entrypoints.https.http.encodequerysemicolons=true",
"--providers.docker=true",
"--providers.docker.exposedbydefault=false",
"--providers.file.directory=/traefik/dynamic/",
"--providers.file.watch=true",
@@ -86,16 +125,7 @@ function generate_default_proxy_configuration(Server $server)
"--certificatesresolvers.letsencrypt.acme.storage=/traefik/acme.json",
"--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=http",
],
"labels" => [
"traefik.enable=true",
"traefik.http.routers.traefik.entrypoints=http",
"traefik.http.routers.traefik.middlewares=traefik-basic-auth@file",
"traefik.http.routers.traefik.service=api@internal",
"traefik.http.services.traefik.loadbalancer.server.port=8080",
// Global Middlewares
"traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https",
"traefik.http.middlewares.gzip.compress=true",
],
"labels" => $labels,
],
],
];
@@ -104,7 +134,24 @@ function generate_default_proxy_configuration(Server $server)
$config['services']['traefik']['command'][] = "--accesslog.filepath=/traefik/access.log";
$config['services']['traefik']['command'][] = "--accesslog.bufferingsize=100";
}
$config = Yaml::dump($config, 4, 2);
if ($server->isSwarm()) {
data_forget($config, 'services.traefik.container_name');
data_forget($config, 'services.traefik.restart');
data_forget($config, 'services.traefik.labels');
$config['services']['traefik']['command'][] = "--providers.docker.swarmMode=true";
$config['services']['traefik']['deploy'] = [
"labels" => $labels,
"placement" => [
"constraints" => [
"node.role==manager",
],
],
];
} else {
$config['services']['traefik']['command'][] = "--providers.docker=true";
}
$config = Yaml::dump($config, 12, 2);
SaveConfiguration::run($server, $config);
return $config;
}

View File

@@ -123,6 +123,9 @@ function instant_remote_process(Collection|array $command, Server $server, $thro
}
return excludeCertainErrors($process->errorOutput(), $exitCode);
}
if ($output === 'null') {
$output = null;
}
return $output;
}
function excludeCertainErrors(string $errorOutput, ?int $exitCode = null)
@@ -151,6 +154,7 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d
if (is_null($application_deployment_queue)) {
return collect([]);
}
// ray(data_get($application_deployment_queue, 'logs'));
try {
$decoded = json_decode(
data_get($application_deployment_queue, 'logs'),
@@ -160,20 +164,24 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d
} catch (\JsonException $exception) {
return collect([]);
}
// ray($decoded );
$formatted = collect($decoded);
if (!$is_debug_enabled) {
$formatted = $formatted->filter(fn ($i) => $i['hidden'] === false ?? false);
}
$formatted = $formatted
->sortBy(fn ($i) => $i['order'])
->sortBy(fn ($i) => data_get($i, 'order'))
->map(function ($i) {
$i['timestamp'] = Carbon::parse($i['timestamp'])->format('Y-M-d H:i:s.u');
data_set($i, 'timestamp', Carbon::parse(data_get($i, 'timestamp'))->format('Y-M-d H:i:s.u'));
return $i;
});
return $formatted;
}
function remove_iip($text)
{
$text = preg_replace('/x-access-token:.*?(?=@)/', "x-access-token:" . REDACTED, $text);
return preg_replace('/\x1b\[[0-9;]*m/', '', $text);
}
function refresh_server_connection(?PrivateKey $private_key = null)
{
if (is_null($private_key)) {

File diff suppressed because it is too large Load Diff

View File

@@ -22,13 +22,13 @@ return [
'official' => 'https://cdn.coollabs.io/coolify/service-templates.json',
],
'limits' => [
'trial_period' => 7,
'trial_period' => 0,
'server' => [
'zero' => 0,
'self-hosted' => 999999999999,
'basic' => 1,
'pro' => 10,
'ultimate' => 25,
'basic' => env('LIMIT_SERVER_BASIC', 2),
'pro' => env('LIMIT_SERVER_PRO', 10),
'ultimate' => env('LIMIT_SERVER_ULTIMATE', 25),
],
'email' => [
'zero' => true,

View File

@@ -3,11 +3,11 @@
return [
// @see https://docs.sentry.io/product/sentry-basics/dsn-explainer/
'dsn' => 'https://c35fe90ee56e18b220bb55e8217d4839@o1082494.ingest.sentry.io/4505347448045568',
'dsn' => 'https://bea22abf110618b07252032aa2e07859@o1082494.ingest.sentry.io/4505347448045568',
// The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => '4.0.0-beta.139',
'release' => '4.0.0-beta.152',
// When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'),
@@ -76,6 +76,7 @@ return [
'send_default_pii' => env('SENTRY_SEND_DEFAULT_PII', false),
// @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#traces-sample-rate
'enable_tracing' => env('SENTRY_ENABLE_TRACING', false),
'traces_sample_rate' => 0.2,
'profiles_sample_rate' => env('SENTRY_PROFILES_SAMPLE_RATE') === null ? null : (float)env('SENTRY_PROFILES_SAMPLE_RATE'),

View File

@@ -1,3 +1,3 @@
<?php
return '4.0.0-beta.139';
return '4.0.0-beta.152';

View File

@@ -0,0 +1,76 @@
<?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_log_drain_enabled')->default(false);
});
Schema::table('standalone_redis', function (Blueprint $table) {
$table->boolean('is_log_drain_enabled')->default(false);
});
Schema::table('standalone_mysqls', function (Blueprint $table) {
$table->boolean('is_log_drain_enabled')->default(false);
});
Schema::table('standalone_mariadbs', function (Blueprint $table) {
$table->boolean('is_log_drain_enabled')->default(false);
});
Schema::table('standalone_postgresqls', function (Blueprint $table) {
$table->boolean('is_log_drain_enabled')->default(false);
});
Schema::table('standalone_mongodbs', function (Blueprint $table) {
$table->boolean('is_log_drain_enabled')->default(false);
});
Schema::table('service_applications', function (Blueprint $table) {
$table->boolean('is_log_drain_enabled')->default(false);
});
Schema::table('service_databases', function (Blueprint $table) {
$table->boolean('is_log_drain_enabled')->default(false);
});
Schema::table('servers', function (Blueprint $table) {
$table->boolean('log_drain_notification_sent')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('application_settings', function (Blueprint $table) {
$table->dropColumn('is_log_drain_enabled');
});
Schema::table('standalone_redis', function (Blueprint $table) {
$table->dropColumn('is_log_drain_enabled');
});
Schema::table('standalone_mysqls', function (Blueprint $table) {
$table->dropColumn('is_log_drain_enabled');
});
Schema::table('standalone_mariadbs', function (Blueprint $table) {
$table->dropColumn('is_log_drain_enabled');
});
Schema::table('standalone_postgresqls', function (Blueprint $table) {
$table->dropColumn('is_log_drain_enabled');
});
Schema::table('standalone_mongodbs', function (Blueprint $table) {
$table->dropColumn('is_log_drain_enabled');
});
Schema::table('service_applications', function (Blueprint $table) {
$table->dropColumn('is_log_drain_enabled');
});
Schema::table('service_databases', function (Blueprint $table) {
$table->dropColumn('is_log_drain_enabled');
});
Schema::table('servers', function (Blueprint $table) {
$table->dropColumn('log_drain_notification_sent');
});
}
};

View File

@@ -0,0 +1,36 @@
<?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_gpu_enabled')->default(false);
$table->string('gpu_driver')->default('nvidia');
$table->string('gpu_count')->nullable();
$table->string('gpu_device_ids')->nullable();
$table->longText('gpu_options')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('application_settings', function (Blueprint $table) {
$table->dropColumn('is_gpu_enabled');
$table->dropColumn('gpu_driver');
$table->dropColumn('gpu_count');
$table->dropColumn('gpu_device_ids');
$table->dropColumn('gpu_options');
});
}
};

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('applications', function (Blueprint $table) {
$table->string('additional_destinations')->nullable()->after('destination');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->dropColumn('additional_destinations');
});
}
};

View File

@@ -0,0 +1,42 @@
<?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('applications', function (Blueprint $table) {
$table->string('docker_compose_location')->nullable()->default('/docker-compose.yaml')->after('dockerfile_location');
$table->string('docker_compose_pr_location')->nullable()->default('/docker-compose.yaml')->after('docker_compose_location');
$table->longText('docker_compose')->nullable()->after('docker_compose_location');
$table->longText('docker_compose_pr')->nullable()->after('docker_compose_location');
$table->longText('docker_compose_raw')->nullable()->after('docker_compose');
$table->longText('docker_compose_pr_raw')->nullable()->after('docker_compose');
$table->text('docker_compose_domains')->nullable()->after('docker_compose_raw');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->dropColumn('docker_compose_location');
$table->dropColumn('docker_compose_pr_location');
$table->dropColumn('docker_compose');
$table->dropColumn('docker_compose_pr');
$table->dropColumn('docker_compose_raw');
$table->dropColumn('docker_compose_pr_raw');
$table->dropColumn('docker_compose_domains');
});
}
};

View File

@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('swarm_dockers', function (Blueprint $table) {
$table->string('network');
$table->unique(['server_id', 'network']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('swarm_dockers', function (Blueprint $table) {
$table->dropColumn('network');
});
}
};

View File

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

View File

@@ -0,0 +1,64 @@
<?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_include_timestamps')->default(false);
});
Schema::table('service_applications', function (Blueprint $table) {
$table->boolean('is_include_timestamps')->default(false);
});
Schema::table('service_databases', function (Blueprint $table) {
$table->boolean('is_include_timestamps')->default(false);
});
Schema::table('standalone_mysqls', function (Blueprint $table) {
$table->boolean('is_include_timestamps')->default(false);
});
Schema::table('standalone_postgresqls', function (Blueprint $table) {
$table->boolean('is_include_timestamps')->default(false);
});
Schema::table('standalone_redis', function (Blueprint $table) {
$table->boolean('is_include_timestamps')->default(false);
});
Schema::table('standalone_mongodbs', function (Blueprint $table) {
$table->boolean('is_include_timestamps')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('application_settings', function (Blueprint $table) {
$table->dropColumn('is_include_timestamps');
});
Schema::table('service_applications', function (Blueprint $table) {
$table->dropColumn('is_include_timestamps');
});
Schema::table('service_databases', function (Blueprint $table) {
$table->dropColumn('is_include_timestamps');
});
Schema::table('standalone_mysqls', function (Blueprint $table) {
$table->dropColumn('is_include_timestamps');
});
Schema::table('standalone_postgresqls', function (Blueprint $table) {
$table->dropColumn('is_include_timestamps');
});
Schema::table('standalone_redis', function (Blueprint $table) {
$table->dropColumn('is_include_timestamps');
});
Schema::table('standalone_mongodbs', function (Blueprint $table) {
$table->dropColumn('is_include_timestamps');
});
}
};

View File

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

View File

@@ -13,9 +13,9 @@ class SwarmDockerSeeder extends Seeder
*/
public function run(): void
{
SwarmDocker::create([
'name' => 'Swarm Docker 1',
'server_id' => 1,
]);
// SwarmDocker::create([
// 'name' => 'Swarm Docker 1',
// 'server_id' => 1,
// ]);
}
}

View File

@@ -28,6 +28,8 @@ services:
- REDIS_HOST
- REDIS_PASSWORD
- HORIZON_MAX_PROCESSES
- HORIZON_BALANCE_MAX_SHIFT
- HORIZON_BALANCE_COOLDOWN
- SSL_MODE=off
- PHP_PM_CONTROL=dynamic
- PHP_PM_START_SERVERS=1

View File

@@ -1,2 +1,2 @@
#!/command/execlineb -P
php /var/www/html/artisan app:init
php /var/www/html/artisan app:init --cleanup

265
package-lock.json generated
View File

@@ -6,19 +6,19 @@
"": {
"dependencies": {
"@tailwindcss/typography": "0.5.10",
"alpinejs": "3.13.1",
"daisyui": "4.0.3",
"alpinejs": "3.13.2",
"daisyui": "4.3.1",
"tailwindcss-scrollbar": "0.1.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "4.4.0",
"@vitejs/plugin-vue": "4.5.0",
"autoprefixer": "10.4.16",
"axios": "1.5.1",
"axios": "1.6.2",
"laravel-vite-plugin": "0.8.1",
"postcss": "8.4.31",
"tailwindcss": "3.3.5",
"vite": "4.4.11",
"vue": "3.3.4"
"vite": "4.5.0",
"vue": "3.3.8"
}
},
"node_modules/@alloc/quick-lru": {
@@ -33,9 +33,9 @@
}
},
"node_modules/@babel/parser": {
"version": "7.22.4",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.4.tgz",
"integrity": "sha512-VLLsx06XkEYqBtE5YGPwfSGwfrjnyPP5oiGty3S8pQLFDFLaS8VwWSIxkTXpcvr5zeYLE6+MBNl2npl/YnfofA==",
"version": "7.23.3",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.3.tgz",
"integrity": "sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==",
"dev": true,
"bin": {
"parser": "bin/babel-parser.js"
@@ -503,90 +503,90 @@
}
},
"node_modules/@vitejs/plugin-vue": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.4.0.tgz",
"integrity": "sha512-xdguqb+VUwiRpSg+nsc2HtbAUSGak25DXYvpQQi4RVU1Xq1uworyoH/md9Rfd8zMmPR/pSghr309QNcftUVseg==",
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.5.0.tgz",
"integrity": "sha512-a2WSpP8X8HTEww/U00bU4mX1QpLINNuz/2KMNpLsdu3BzOpak3AGI1CJYBTXcc4SPhaD0eNRUp7IyQK405L5dQ==",
"dev": true,
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"peerDependencies": {
"vite": "^4.0.0",
"vite": "^4.0.0 || ^5.0.0",
"vue": "^3.2.25"
}
},
"node_modules/@vue/compiler-core": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.4.tgz",
"integrity": "sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==",
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.8.tgz",
"integrity": "sha512-hN/NNBUECw8SusQvDSqqcVv6gWq8L6iAktUR0UF3vGu2OhzRqcOiAno0FmBJWwxhYEXRlQJT5XnoKsVq1WZx4g==",
"dev": true,
"dependencies": {
"@babel/parser": "^7.21.3",
"@vue/shared": "3.3.4",
"@babel/parser": "^7.23.0",
"@vue/shared": "3.3.8",
"estree-walker": "^2.0.2",
"source-map-js": "^1.0.2"
}
},
"node_modules/@vue/compiler-core/node_modules/@vue/shared": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz",
"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==",
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.8.tgz",
"integrity": "sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw==",
"dev": true
},
"node_modules/@vue/compiler-dom": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.4.tgz",
"integrity": "sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==",
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.8.tgz",
"integrity": "sha512-+PPtv+p/nWDd0AvJu3w8HS0RIm/C6VGBIRe24b9hSyNWOAPEUosFZ5diwawwP8ip5sJ8n0Pe87TNNNHnvjs0FQ==",
"dev": true,
"dependencies": {
"@vue/compiler-core": "3.3.4",
"@vue/shared": "3.3.4"
"@vue/compiler-core": "3.3.8",
"@vue/shared": "3.3.8"
}
},
"node_modules/@vue/compiler-dom/node_modules/@vue/shared": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz",
"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==",
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.8.tgz",
"integrity": "sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw==",
"dev": true
},
"node_modules/@vue/compiler-sfc": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.4.tgz",
"integrity": "sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==",
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.8.tgz",
"integrity": "sha512-WMzbUrlTjfYF8joyT84HfwwXo+8WPALuPxhy+BZ6R4Aafls+jDBnSz8PDz60uFhuqFbl3HxRfxvDzrUf3THwpA==",
"dev": true,
"dependencies": {
"@babel/parser": "^7.20.15",
"@vue/compiler-core": "3.3.4",
"@vue/compiler-dom": "3.3.4",
"@vue/compiler-ssr": "3.3.4",
"@vue/reactivity-transform": "3.3.4",
"@vue/shared": "3.3.4",
"@babel/parser": "^7.23.0",
"@vue/compiler-core": "3.3.8",
"@vue/compiler-dom": "3.3.8",
"@vue/compiler-ssr": "3.3.8",
"@vue/reactivity-transform": "3.3.8",
"@vue/shared": "3.3.8",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.0",
"postcss": "^8.1.10",
"magic-string": "^0.30.5",
"postcss": "^8.4.31",
"source-map-js": "^1.0.2"
}
},
"node_modules/@vue/compiler-sfc/node_modules/@vue/shared": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz",
"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==",
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.8.tgz",
"integrity": "sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw==",
"dev": true
},
"node_modules/@vue/compiler-ssr": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz",
"integrity": "sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==",
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.8.tgz",
"integrity": "sha512-hXCqQL/15kMVDBuoBYpUnSYT8doDNwsjvm3jTefnXr+ytn294ySnT8NlsFHmTgKNjwpuFy7XVV8yTeLtNl/P6w==",
"dev": true,
"dependencies": {
"@vue/compiler-dom": "3.3.4",
"@vue/shared": "3.3.4"
"@vue/compiler-dom": "3.3.8",
"@vue/shared": "3.3.8"
}
},
"node_modules/@vue/compiler-ssr/node_modules/@vue/shared": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz",
"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==",
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.8.tgz",
"integrity": "sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw==",
"dev": true
},
"node_modules/@vue/reactivity": {
@@ -598,83 +598,83 @@
}
},
"node_modules/@vue/reactivity-transform": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz",
"integrity": "sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==",
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.8.tgz",
"integrity": "sha512-49CvBzmZNtcHua0XJ7GdGifM8GOXoUMOX4dD40Y5DxI3R8OUhMlvf2nvgUAcPxaXiV5MQQ1Nwy09ADpnLQUqRw==",
"dev": true,
"dependencies": {
"@babel/parser": "^7.20.15",
"@vue/compiler-core": "3.3.4",
"@vue/shared": "3.3.4",
"@babel/parser": "^7.23.0",
"@vue/compiler-core": "3.3.8",
"@vue/shared": "3.3.8",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.0"
"magic-string": "^0.30.5"
}
},
"node_modules/@vue/reactivity-transform/node_modules/@vue/shared": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz",
"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==",
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.8.tgz",
"integrity": "sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw==",
"dev": true
},
"node_modules/@vue/runtime-core": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.4.tgz",
"integrity": "sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==",
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.8.tgz",
"integrity": "sha512-qurzOlb6q26KWQ/8IShHkMDOuJkQnQcTIp1sdP4I9MbCf9FJeGVRXJFr2mF+6bXh/3Zjr9TDgURXrsCr9bfjUw==",
"dev": true,
"dependencies": {
"@vue/reactivity": "3.3.4",
"@vue/shared": "3.3.4"
"@vue/reactivity": "3.3.8",
"@vue/shared": "3.3.8"
}
},
"node_modules/@vue/runtime-core/node_modules/@vue/reactivity": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.4.tgz",
"integrity": "sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==",
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.8.tgz",
"integrity": "sha512-ctLWitmFBu6mtddPyOKpHg8+5ahouoTCRtmAHZAXmolDtuZXfjL2T3OJ6DL6ezBPQB1SmMnpzjiWjCiMYmpIuw==",
"dev": true,
"dependencies": {
"@vue/shared": "3.3.4"
"@vue/shared": "3.3.8"
}
},
"node_modules/@vue/runtime-core/node_modules/@vue/shared": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz",
"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==",
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.8.tgz",
"integrity": "sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw==",
"dev": true
},
"node_modules/@vue/runtime-dom": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.4.tgz",
"integrity": "sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==",
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.8.tgz",
"integrity": "sha512-Noy5yM5UIf9UeFoowBVgghyGGPIDPy1Qlqt0yVsUdAVbqI8eeMSsTqBtauaEoT2UFXUk5S64aWVNJN4MJ2vRdA==",
"dev": true,
"dependencies": {
"@vue/runtime-core": "3.3.4",
"@vue/shared": "3.3.4",
"csstype": "^3.1.1"
"@vue/runtime-core": "3.3.8",
"@vue/shared": "3.3.8",
"csstype": "^3.1.2"
}
},
"node_modules/@vue/runtime-dom/node_modules/@vue/shared": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz",
"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==",
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.8.tgz",
"integrity": "sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw==",
"dev": true
},
"node_modules/@vue/server-renderer": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.4.tgz",
"integrity": "sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==",
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.8.tgz",
"integrity": "sha512-zVCUw7RFskvPuNlPn/8xISbrf0zTWsTSdYTsUTN1ERGGZGVnRxM2QZ3x1OR32+vwkkCm0IW6HmJ49IsPm7ilLg==",
"dev": true,
"dependencies": {
"@vue/compiler-ssr": "3.3.4",
"@vue/shared": "3.3.4"
"@vue/compiler-ssr": "3.3.8",
"@vue/shared": "3.3.8"
},
"peerDependencies": {
"vue": "3.3.4"
"vue": "3.3.8"
}
},
"node_modules/@vue/server-renderer/node_modules/@vue/shared": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz",
"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==",
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.8.tgz",
"integrity": "sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw==",
"dev": true
},
"node_modules/@vue/shared": {
@@ -683,9 +683,9 @@
"integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA=="
},
"node_modules/alpinejs": {
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.13.1.tgz",
"integrity": "sha512-/LZ7mumW02V7AV5xTTftJFHS0I3KOXLl7tHm4xpxXAV+HJ/zjTT0n8MU7RZ6UoGPhmO/i+KEhQojaH/0RsH5tg==",
"version": "3.13.2",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.13.2.tgz",
"integrity": "sha512-WzojeeN082kLZznGI1HAuP8yFJSWqJ1fGdz2mUjj45H4y0XwToE7fFqtI3mCPRR+BpcSbxT/NL+FyPnYAWSltw==",
"dependencies": {
"@vue/reactivity": "~3.1.1"
}
@@ -756,9 +756,9 @@
}
},
"node_modules/axios": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz",
"integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==",
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz",
"integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==",
"dev": true,
"dependencies": {
"follow-redirects": "^1.15.0",
@@ -956,9 +956,9 @@
}
},
"node_modules/daisyui": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.0.3.tgz",
"integrity": "sha512-mG6PsdIA6MEHzdJwBlJxc1rqsIAAlcfhj2O8g0ol1uWg5y6C5zTcqfG8vKBqK4y2YfCxGIVgMsMWRTSm32N1Ow==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.3.1.tgz",
"integrity": "sha512-dCi91VD+57lkoBd10CjdW4wPOeOPYvvzQbxti6xmyQbDMbCeCXwNq2KdoU798I4OsCcD5B+n7yVG7HAgYW+cvw==",
"dependencies": {
"css-selector-tokenizer": "^0.8",
"culori": "^3",
@@ -1153,9 +1153,9 @@
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"hasInstallScript": true,
"optional": true,
"os": [
@@ -1335,12 +1335,12 @@
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
},
"node_modules/magic-string": {
"version": "0.30.0",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz",
"integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==",
"version": "0.30.5",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz",
"integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==",
"dev": true,
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.13"
"@jridgewell/sourcemap-codec": "^1.4.15"
},
"engines": {
"node": ">=12"
@@ -1711,9 +1711,9 @@
}
},
"node_modules/rollup": {
"version": "3.29.3",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.3.tgz",
"integrity": "sha512-T7du6Hum8jOkSWetjRgbwpM6Sy0nECYrYRSmZjayFcOddtKJWU4d17AC3HNUk7HRuqy4p+G7aEZclSHytqUmEg==",
"version": "3.29.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz",
"integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==",
"dev": true,
"bin": {
"rollup": "dist/bin/rollup"
@@ -1922,9 +1922,9 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/vite": {
"version": "4.4.11",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.4.11.tgz",
"integrity": "sha512-ksNZJlkcU9b0lBwAGZGGaZHCMqHsc8OpgtoYhsQ4/I2v5cnpmmmqe5pM4nv/4Hn6G/2GhTdj0DhZh2e+Er1q5A==",
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz",
"integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==",
"dev": true,
"dependencies": {
"esbuild": "^0.18.10",
@@ -1977,35 +1977,40 @@
}
},
"node_modules/vite-plugin-full-reload": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/vite-plugin-full-reload/-/vite-plugin-full-reload-1.0.5.tgz",
"integrity": "sha512-kVZFDFWr0DxiHn6MuDVTQf7gnWIdETGlZh0hvTiMXzRN80vgF4PKbONSq8U1d0WtHsKaFODTQgJeakLacoPZEQ==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/vite-plugin-full-reload/-/vite-plugin-full-reload-1.1.0.tgz",
"integrity": "sha512-3cObNDzX6DdfhD9E7kf6w2mNunFpD7drxyNgHLw+XwIYAgb+Xt16SEXo0Up4VH+TMf3n+DSVJZtW2POBGcBYAA==",
"dev": true,
"dependencies": {
"picocolors": "^1.0.0",
"picomatch": "^2.3.1"
},
"peerDependencies": {
"vite": "^2 || ^3 || ^4"
}
},
"node_modules/vue": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.3.4.tgz",
"integrity": "sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==",
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.3.8.tgz",
"integrity": "sha512-5VSX/3DabBikOXMsxzlW8JyfeLKlG9mzqnWgLQLty88vdZL7ZJgrdgBOmrArwxiLtmS+lNNpPcBYqrhE6TQW5w==",
"dev": true,
"dependencies": {
"@vue/compiler-dom": "3.3.4",
"@vue/compiler-sfc": "3.3.4",
"@vue/runtime-dom": "3.3.4",
"@vue/server-renderer": "3.3.4",
"@vue/shared": "3.3.4"
"@vue/compiler-dom": "3.3.8",
"@vue/compiler-sfc": "3.3.8",
"@vue/runtime-dom": "3.3.8",
"@vue/server-renderer": "3.3.8",
"@vue/shared": "3.3.8"
},
"peerDependencies": {
"typescript": "*"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/vue/node_modules/@vue/shared": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz",
"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==",
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.8.tgz",
"integrity": "sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw==",
"dev": true
},
"node_modules/wrappy": {

View File

@@ -6,19 +6,19 @@
"build": "vite build"
},
"devDependencies": {
"@vitejs/plugin-vue": "4.4.0",
"@vitejs/plugin-vue": "4.5.0",
"autoprefixer": "10.4.16",
"axios": "1.5.1",
"axios": "1.6.2",
"laravel-vite-plugin": "0.8.1",
"postcss": "8.4.31",
"tailwindcss": "3.3.5",
"vite": "4.4.11",
"vue": "3.3.4"
"vite": "4.5.0",
"vue": "3.3.8"
},
"dependencies": {
"@tailwindcss/typography": "0.5.10",
"alpinejs": "3.13.1",
"daisyui": "4.0.3",
"alpinejs": "3.13.2",
"daisyui": "4.3.1",
"tailwindcss-scrollbar": "0.1.0"
}
}

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