mirror of
https://github.com/ershisan99/coolify.git
synced 2025-12-18 12:33:06 +00:00
Compare commits
205 Commits
v4.0.0-bet
...
v4.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f69eb3c2e | ||
|
|
6ccbf911b2 | ||
|
|
5c77cec68f | ||
|
|
25a0489f7f | ||
|
|
5e27b88bef | ||
|
|
ec98afe707 | ||
|
|
ce26127705 | ||
|
|
ef7fc1b260 | ||
|
|
f58e6766e1 | ||
|
|
4a21102983 | ||
|
|
e78b6758d8 | ||
|
|
16eb7f4fb4 | ||
|
|
4974ce6eda | ||
|
|
6cdba17aca | ||
|
|
30f8e8f232 | ||
|
|
608f0b7840 | ||
|
|
d0366c4054 | ||
|
|
f88e3c5b29 | ||
|
|
e33fec0e1a | ||
|
|
912b0a263e | ||
|
|
8f963adbd4 | ||
|
|
8f2c24d7e9 | ||
|
|
9f3dbc3cbb | ||
|
|
8a9ee84925 | ||
|
|
689480003a | ||
|
|
3b20eee909 | ||
|
|
e8cadc176b | ||
|
|
b0c96e64c9 | ||
|
|
9ce3b43e09 | ||
|
|
4c2b3df861 | ||
|
|
467471f54a | ||
|
|
60171093c5 | ||
|
|
38f2a2dac7 | ||
|
|
307ee52ac0 | ||
|
|
b66c9835b7 | ||
|
|
d38d50dca2 | ||
|
|
40023be4ea | ||
|
|
48d7c6e76f | ||
|
|
debacfe2f7 | ||
|
|
d430813230 | ||
|
|
e30c37b041 | ||
|
|
8c73068cc7 | ||
|
|
2c4e69ad50 | ||
|
|
5ae08d009e | ||
|
|
673b944647 | ||
|
|
16281248ac | ||
|
|
8670b41671 | ||
|
|
9c69044da5 | ||
|
|
ebc4ab9af5 | ||
|
|
57738198ad | ||
|
|
b8252b85b0 | ||
|
|
479c2743bd | ||
|
|
81e6482d7a | ||
|
|
88c5d87084 | ||
|
|
6c7e091e1b | ||
|
|
91e3d33c0b | ||
|
|
aa00389824 | ||
|
|
b4e54ab3e3 | ||
|
|
8f3c5d4bd3 | ||
|
|
26668c71a1 | ||
|
|
bd7637c696 | ||
|
|
cff54f48a3 | ||
|
|
5c0f239f62 | ||
|
|
d56c28c8d9 | ||
|
|
2b666ff121 | ||
|
|
fb42c43953 | ||
|
|
81437e6822 | ||
|
|
2fe429fe92 | ||
|
|
4f0b214042 | ||
|
|
c866213f34 | ||
|
|
7cec6330cf | ||
|
|
f5de21a343 | ||
|
|
ecbfc4d790 | ||
|
|
55ff00e028 | ||
|
|
a0fc2bbb85 | ||
|
|
51a704b22a | ||
|
|
6d49678842 | ||
|
|
0459b3a115 | ||
|
|
82592c8222 | ||
|
|
25bf8895e2 | ||
|
|
f4f7bdf7d5 | ||
|
|
c008564aa3 | ||
|
|
b825d98b2d | ||
|
|
1f711d9281 | ||
|
|
1de850f640 | ||
|
|
f176247b02 | ||
|
|
3f3a1283df | ||
|
|
087bfcad08 | ||
|
|
efd2899ae3 | ||
|
|
e4b2195932 | ||
|
|
0590ed7b2e | ||
|
|
3a3c9448a4 | ||
|
|
36d65ad5a8 | ||
|
|
8db66952e8 | ||
|
|
45fa88ca4d | ||
|
|
84b74f0b57 | ||
|
|
423cf62d92 | ||
|
|
c4d9deabef | ||
|
|
776b1cb68d | ||
|
|
fc3025398e | ||
|
|
457c16c4dc | ||
|
|
ccf63c67e8 | ||
|
|
945157b30c | ||
|
|
13798392be | ||
|
|
0d05b0a3d6 | ||
|
|
e0d2f88d99 | ||
|
|
e260bfae02 | ||
|
|
5abd4a6d78 | ||
|
|
9dff1e5631 | ||
|
|
02332ade1b | ||
|
|
486de58d5b | ||
|
|
606aeb2b61 | ||
|
|
3fc264560c | ||
|
|
3dd9182281 | ||
|
|
c838ff7198 | ||
|
|
ca6db9c1a9 | ||
|
|
f27e00e80e | ||
|
|
60cf296f31 | ||
|
|
ea64e9d5ad | ||
|
|
55846c5635 | ||
|
|
7763594e6e | ||
|
|
6b5339c1c1 | ||
|
|
f2980738e4 | ||
|
|
f0e3ad0461 | ||
|
|
187050e098 | ||
|
|
9e7823795d | ||
|
|
239459dfa8 | ||
|
|
ce0f560c44 | ||
|
|
95baec99dd | ||
|
|
363e8fc0b5 | ||
|
|
e49caba920 | ||
|
|
30db2b2a09 | ||
|
|
285666e181 | ||
|
|
003934ee1d | ||
|
|
44c7958aa6 | ||
|
|
35b1a81dfe | ||
|
|
e40f397cc7 | ||
|
|
9fd8cd7e6c | ||
|
|
a94b7ee611 | ||
|
|
fc68bf50b5 | ||
|
|
0f99ee787c | ||
|
|
95777e978e | ||
|
|
fb0b9dbfed | ||
|
|
9617000daa | ||
|
|
1818404172 | ||
|
|
d9a966fd98 | ||
|
|
763ce5fc14 | ||
|
|
df021760a7 | ||
|
|
fb2598f2e4 | ||
|
|
7af07b2718 | ||
|
|
23a94c9378 | ||
|
|
ed34fc9645 | ||
|
|
cafd9e0ab2 | ||
|
|
e882477e21 | ||
|
|
db0e3cfcc4 | ||
|
|
b3c4429028 | ||
|
|
87ab4bd71e | ||
|
|
61e1fdede9 | ||
|
|
b189919f97 | ||
|
|
8f5b084931 | ||
|
|
eb96a5ae7b | ||
|
|
f0fb9dbb94 | ||
|
|
cb2d4b4a0a | ||
|
|
3aace2d4f9 | ||
|
|
8c2ed75653 | ||
|
|
8c8aafbc65 | ||
|
|
a2c39fd07e | ||
|
|
9698a051d9 | ||
|
|
51423394ba | ||
|
|
8e5e36dd5b | ||
|
|
c1dd05dcd8 | ||
|
|
dd1ce6ee6c | ||
|
|
fe4c6d396c | ||
|
|
8db54ec069 | ||
|
|
3abc720926 | ||
|
|
64cc0b63f1 | ||
|
|
c78068466b | ||
|
|
88e407756d | ||
|
|
1538116e6e | ||
|
|
aba47d58a4 | ||
|
|
e7f184dd82 | ||
|
|
2ad8d7812b | ||
|
|
8212bb99a1 | ||
|
|
5fc382d09d | ||
|
|
78a80c46da | ||
|
|
c365d132af | ||
|
|
4dc3db3845 | ||
|
|
b9427d2ec1 | ||
|
|
332a0b9e04 | ||
|
|
18e98aaf52 | ||
|
|
a7f9fad627 | ||
|
|
b01f6ac414 | ||
|
|
e1bc2cc406 | ||
|
|
74830b12f3 | ||
|
|
56a977c676 | ||
|
|
a0bb5733e6 | ||
|
|
516e10ddf2 | ||
|
|
2976c72e09 | ||
|
|
7377e9e415 | ||
|
|
d77c55148b | ||
|
|
ad7aa2eed6 | ||
|
|
5cec50efbe | ||
|
|
ef8686d4da | ||
|
|
8ae18f49dc | ||
|
|
4feb99cbe0 |
22
.tinkerwell/snippets/DeleteUser.php
Normal file
22
.tinkerwell/snippets/DeleteUser.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
$email = 'test@example.com';
|
||||
$user = User::whereEmail($email)->first();
|
||||
$teams = $user->teams;
|
||||
foreach ($teams as $team) {
|
||||
$servers = $team->servers;
|
||||
if ($servers->count() > 0) {
|
||||
foreach ($servers as $server) {
|
||||
dump($server);
|
||||
$server->delete();
|
||||
}
|
||||
}
|
||||
dump($team);
|
||||
$team->delete();
|
||||
}
|
||||
if ($user) {
|
||||
dump($user);
|
||||
$user->delete();
|
||||
}
|
||||
@@ -9,7 +9,7 @@ use Illuminate\Support\Facades\Mail;
|
||||
|
||||
set_transanctional_email_settings();
|
||||
|
||||
$users = User::whereEmail('andras.bacsai@gmail.com');
|
||||
$users = User::whereEmail('test@example.com');
|
||||
foreach ($users as $user) {
|
||||
Mail::send([], [], function ($message) use ($user) {
|
||||
$message
|
||||
|
||||
35
README.md
35
README.md
@@ -10,35 +10,40 @@ No vendor lock-in, which means that all the configuration for your applications/
|
||||
|
||||
For more information, take a look at our landing page [here](https://coolify.io).
|
||||
|
||||
> If you are looking for previous (v3) version, it is [here](https://github.com/coollabsio/coolify/tree/v3).
|
||||
# Donations
|
||||
To stay completely free, open-source, no feature behind paywall and evolve the project, we need your help. If you like Coolify, please consider donating to help us fund the future development of the project.
|
||||
|
||||
https://coolify.io/sponsorships
|
||||
|
||||
Thank you so much!
|
||||
|
||||
# Cloud
|
||||
|
||||
If you do not want to self-host Coolify, there is a paid cloud version available: https://app.coolify.io
|
||||
|
||||
You can easily attach your own servers, get all the automations, free email notifications, etc.
|
||||
|
||||
For more information & pricing, take a look at our landing page [here](https://coolify.io).
|
||||
|
||||
# Beta
|
||||
## Why should I use the Cloud version?
|
||||
The recommended way to use Coolify is to have one server for Coolify and one (or more) for the resources you are deploying. A server is around 4-5$/month.
|
||||
|
||||
The latest version (v4) is still in beta. That does not mean it is unstable. All the features that are available are stable enough be usable in real-life.
|
||||
|
||||
There are hundreds of people using it for managing their client's applications, freelancers, hobbyists, businesses.
|
||||
By subscribing to the cloud version, you get the Coolify server for the same price, but with:
|
||||
- High-availability
|
||||
- Free email notifications
|
||||
- Better support
|
||||
- Less maintenance for you
|
||||
|
||||
# Installation
|
||||
|
||||
```bash
|
||||
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
|
||||
```
|
||||
You can find the installation script source [here](./scripts/install.sh).
|
||||
|
||||
You can find the installation script [here](./scripts/install.sh).
|
||||
|
||||
## Support
|
||||
# Support
|
||||
|
||||
Contact us [here](https://coolify.io/docs/contact).
|
||||
|
||||
## Recognitions
|
||||
# Recognitions
|
||||
|
||||
<p>
|
||||
<a href="https://news.ycombinator.com/item?id=26624341">
|
||||
@@ -54,11 +59,11 @@ Contact us [here](https://coolify.io/docs/contact).
|
||||
|
||||
<a href="https://trendshift.io/repositories/634" target="_blank"><img src="https://trendshift.io/api/badge/repositories/634" alt="coollabsio%2Fcoolify | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
|
||||
## 💰 Financial Contributors
|
||||
# 💰 Financial Contributors
|
||||
|
||||
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/coollabsio/contribute)]
|
||||
|
||||
### Organizations
|
||||
## Organizations
|
||||
|
||||
Special thanks to our biggest sponsors, [CCCareers](https://cccareers.org/) and [Appwrite](https://appwrite.io)!
|
||||
|
||||
@@ -78,10 +83,10 @@ Support this project with your organization. Your logo will show up here with a
|
||||
<a href="https://opencollective.com/coollabsio/organization/8/website"><img src="https://opencollective.com/coollabsio/organization/8/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/coollabsio/organization/9/website"><img src="https://opencollective.com/coollabsio/organization/9/avatar.svg"></a>
|
||||
|
||||
### Individuals
|
||||
## Individuals
|
||||
|
||||
<a href="https://opencollective.com/coollabsio"><img src="https://opencollective.com/coollabsio/individuals.svg?width=890"></a>
|
||||
|
||||
## Star History
|
||||
# Star History
|
||||
|
||||
[](https://star-history.com/#coollabsio/coolify&Date)
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Actions\Application;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Notifications\Application\StatusChanged;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class StopApplication
|
||||
@@ -12,7 +11,7 @@ class StopApplication
|
||||
public function handle(Application $application)
|
||||
{
|
||||
$server = $application->destination->server;
|
||||
$containers = getCurrentApplicationContainerStatus($server, $application->id);
|
||||
$containers = getCurrentApplicationContainerStatus($server, $application->id, 0);
|
||||
if ($containers->count() > 0) {
|
||||
foreach ($containers as $container) {
|
||||
$containerName = data_get($container, 'Names');
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Actions\Database;
|
||||
|
||||
use App\Models\ServiceDatabase;
|
||||
use App\Models\StandaloneMariadb;
|
||||
use App\Models\StandaloneMongodb;
|
||||
use App\Models\StandaloneMysql;
|
||||
@@ -14,21 +15,53 @@ class StartDatabaseProxy
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $database)
|
||||
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|ServiceDatabase $database)
|
||||
{
|
||||
$internalPort = null;
|
||||
if ($database->getMorphClass() === 'App\Models\StandaloneRedis') {
|
||||
$type = $database->getMorphClass();
|
||||
$network = data_get($database, 'destination.network');
|
||||
$server = data_get($database, 'destination.server');
|
||||
$containerName = data_get($database, 'uuid');
|
||||
$proxyContainerName = "{$database->uuid}-proxy";
|
||||
if ($database->getMorphClass() === 'App\Models\ServiceDatabase') {
|
||||
$databaseType = $database->databaseType();
|
||||
$network = data_get($database, 'service.destination.network');
|
||||
$server = data_get($database, 'service.destination.server');
|
||||
$proxyContainerName = "{$database->service->uuid}-proxy";
|
||||
switch ($databaseType) {
|
||||
case 'standalone-mariadb':
|
||||
$type = 'App\Models\StandaloneMariadb';
|
||||
$containerName = "mariadb-{$database->service->uuid}";
|
||||
break;
|
||||
case 'standalone-mongodb':
|
||||
$type = 'App\Models\StandaloneMongodb';
|
||||
$containerName = "mongodb-{$database->service->uuid}";
|
||||
break;
|
||||
case 'standalone-mysql':
|
||||
$type = 'App\Models\StandaloneMysql';
|
||||
$containerName = "mysql-{$database->service->uuid}";
|
||||
break;
|
||||
case 'standalone-postgresql':
|
||||
$type = 'App\Models\StandalonePostgresql';
|
||||
$containerName = "postgresql-{$database->service->uuid}";
|
||||
break;
|
||||
case 'standalone-redis':
|
||||
$type = 'App\Models\StandaloneRedis';
|
||||
$containerName = "redis-{$database->service->uuid}";
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($type === 'App\Models\StandaloneRedis') {
|
||||
$internalPort = 6379;
|
||||
} else if ($database->getMorphClass() === 'App\Models\StandalonePostgresql') {
|
||||
} else if ($type === 'App\Models\StandalonePostgresql') {
|
||||
$internalPort = 5432;
|
||||
} else if ($database->getMorphClass() === 'App\Models\StandaloneMongodb') {
|
||||
} else if ($type === 'App\Models\StandaloneMongodb') {
|
||||
$internalPort = 27017;
|
||||
} else if ($database->getMorphClass() === 'App\Models\StandaloneMysql') {
|
||||
} else if ($type === 'App\Models\StandaloneMysql') {
|
||||
$internalPort = 3306;
|
||||
} else if ($database->getMorphClass() === 'App\Models\StandaloneMariadb') {
|
||||
} else if ($type === 'App\Models\StandaloneMariadb') {
|
||||
$internalPort = 3306;
|
||||
}
|
||||
$containerName = "{$database->uuid}-proxy";
|
||||
$configuration_dir = database_proxy_dir($database->uuid);
|
||||
$nginxconf = <<<EOF
|
||||
user nginx;
|
||||
@@ -42,7 +75,7 @@ class StartDatabaseProxy
|
||||
stream {
|
||||
server {
|
||||
listen $database->public_port;
|
||||
proxy_pass $database->uuid:$internalPort;
|
||||
proxy_pass $containerName:$internalPort;
|
||||
}
|
||||
}
|
||||
EOF;
|
||||
@@ -54,19 +87,19 @@ class StartDatabaseProxy
|
||||
$docker_compose = [
|
||||
'version' => '3.8',
|
||||
'services' => [
|
||||
$containerName => [
|
||||
$proxyContainerName => [
|
||||
'build' => [
|
||||
'context' => $configuration_dir,
|
||||
'dockerfile' => 'Dockerfile',
|
||||
],
|
||||
'image' => "nginx:stable-alpine",
|
||||
'container_name' => $containerName,
|
||||
'container_name' => $proxyContainerName,
|
||||
'restart' => RESTART_MODE,
|
||||
'ports' => [
|
||||
"$database->public_port:$database->public_port",
|
||||
],
|
||||
'networks' => [
|
||||
$database->destination->network,
|
||||
$network,
|
||||
],
|
||||
'healthcheck' => [
|
||||
'test' => [
|
||||
@@ -81,9 +114,9 @@ class StartDatabaseProxy
|
||||
]
|
||||
],
|
||||
'networks' => [
|
||||
$database->destination->network => [
|
||||
$network => [
|
||||
'external' => true,
|
||||
'name' => $database->destination->network,
|
||||
'name' => $network,
|
||||
'attachable' => true,
|
||||
]
|
||||
]
|
||||
@@ -96,7 +129,8 @@ class StartDatabaseProxy
|
||||
"echo '{$dockerfile_base64}' | base64 -d > $configuration_dir/Dockerfile",
|
||||
"echo '{$nginxconf_base64}' | base64 -d > $configuration_dir/nginx.conf",
|
||||
"echo '{$dockercompose_base64}' | base64 -d > $configuration_dir/docker-compose.yaml",
|
||||
"docker compose --project-directory {$configuration_dir} pull",
|
||||
"docker compose --project-directory {$configuration_dir} up --build -d",
|
||||
], $database->destination->server);
|
||||
], $server);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
];
|
||||
|
||||
@@ -56,7 +56,7 @@ class StartMariadb
|
||||
'memswap_limit' => $this->database->limits_memory_swap,
|
||||
'mem_swappiness' => $this->database->limits_memory_swappiness,
|
||||
'mem_reservation' => $this->database->limits_memory_reservation,
|
||||
'cpus' => $this->database->limits_cpus,
|
||||
'cpus' => (int) $this->database->limits_cpus,
|
||||
'cpuset' => $this->database->limits_cpuset,
|
||||
'cpu_shares' => $this->database->limits_cpu_shares,
|
||||
]
|
||||
@@ -69,6 +69,16 @@ class StartMariadb
|
||||
]
|
||||
]
|
||||
];
|
||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||
$docker_compose['services'][$container_name]['logging'] = [
|
||||
'driver' => 'fluentd',
|
||||
'options' => [
|
||||
'fluentd-address' => "tcp://127.0.0.1:24224",
|
||||
'fluentd-async' => "true",
|
||||
'fluentd-sub-second-precision' => "true",
|
||||
]
|
||||
];
|
||||
}
|
||||
if (count($this->database->ports_mappings_array) > 0) {
|
||||
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
|
||||
}
|
||||
@@ -91,8 +101,10 @@ class StartMariadb
|
||||
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
|
||||
$readme = generate_readme_file($this->database->name, now());
|
||||
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
||||
$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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
];
|
||||
|
||||
@@ -63,7 +63,7 @@ class StartMongodb
|
||||
'memswap_limit' => $this->database->limits_memory_swap,
|
||||
'mem_swappiness' => $this->database->limits_memory_swappiness,
|
||||
'mem_reservation' => $this->database->limits_memory_reservation,
|
||||
'cpus' => $this->database->limits_cpus,
|
||||
'cpus' => (int) $this->database->limits_cpus,
|
||||
'cpuset' => $this->database->limits_cpuset,
|
||||
'cpu_shares' => $this->database->limits_cpu_shares,
|
||||
]
|
||||
@@ -76,6 +76,16 @@ class StartMongodb
|
||||
]
|
||||
]
|
||||
];
|
||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||
$docker_compose['services'][$container_name]['logging'] = [
|
||||
'driver' => 'fluentd',
|
||||
'options' => [
|
||||
'fluentd-address' => "tcp://127.0.0.1:24224",
|
||||
'fluentd-async' => "true",
|
||||
'fluentd-sub-second-precision' => "true",
|
||||
]
|
||||
];
|
||||
}
|
||||
if (count($this->database->ports_mappings_array) > 0) {
|
||||
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
|
||||
}
|
||||
@@ -107,8 +117,10 @@ class StartMongodb
|
||||
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
|
||||
$readme = generate_readme_file($this->database->name, now());
|
||||
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
||||
$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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
];
|
||||
|
||||
@@ -56,7 +56,7 @@ class StartMysql
|
||||
'memswap_limit' => $this->database->limits_memory_swap,
|
||||
'mem_swappiness' => $this->database->limits_memory_swappiness,
|
||||
'mem_reservation' => $this->database->limits_memory_reservation,
|
||||
'cpus' => $this->database->limits_cpus,
|
||||
'cpus' => (int) $this->database->limits_cpus,
|
||||
'cpuset' => $this->database->limits_cpuset,
|
||||
'cpu_shares' => $this->database->limits_cpu_shares,
|
||||
]
|
||||
@@ -69,6 +69,16 @@ class StartMysql
|
||||
]
|
||||
]
|
||||
];
|
||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||
$docker_compose['services'][$container_name]['logging'] = [
|
||||
'driver' => 'fluentd',
|
||||
'options' => [
|
||||
'fluentd-address' => "tcp://127.0.0.1:24224",
|
||||
'fluentd-async' => "true",
|
||||
'fluentd-sub-second-precision' => "true",
|
||||
]
|
||||
];
|
||||
}
|
||||
if (count($this->database->ports_mappings_array) > 0) {
|
||||
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
|
||||
}
|
||||
@@ -91,8 +101,10 @@ class StartMysql
|
||||
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
|
||||
$readme = generate_readme_file($this->database->name, now());
|
||||
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
||||
$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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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/"
|
||||
];
|
||||
@@ -32,6 +32,8 @@ class StartPostgresql
|
||||
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
|
||||
$environment_variables = $this->generate_environment_variables();
|
||||
$this->generate_init_scripts();
|
||||
$this->add_custom_conf();
|
||||
|
||||
$docker_compose = [
|
||||
'version' => '3.8',
|
||||
'services' => [
|
||||
@@ -64,7 +66,7 @@ class StartPostgresql
|
||||
'memswap_limit' => $this->database->limits_memory_swap,
|
||||
'mem_swappiness' => $this->database->limits_memory_swappiness,
|
||||
'mem_reservation' => $this->database->limits_memory_reservation,
|
||||
'cpus' => $this->database->limits_cpus,
|
||||
'cpus' => (int) $this->database->limits_cpus,
|
||||
'cpuset' => $this->database->limits_cpuset,
|
||||
'cpu_shares' => $this->database->limits_cpu_shares,
|
||||
]
|
||||
@@ -77,6 +79,17 @@ class StartPostgresql
|
||||
]
|
||||
]
|
||||
];
|
||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||
ray('Log Drain Enabled');
|
||||
$docker_compose['services'][$container_name]['logging'] = [
|
||||
'driver' => 'fluentd',
|
||||
'options' => [
|
||||
'fluentd-address' => "tcp://127.0.0.1:24224",
|
||||
'fluentd-async' => "true",
|
||||
'fluentd-sub-second-precision' => "true",
|
||||
]
|
||||
];
|
||||
}
|
||||
if (count($this->database->ports_mappings_array) > 0) {
|
||||
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
|
||||
}
|
||||
@@ -96,13 +109,28 @@ class StartPostgresql
|
||||
];
|
||||
}
|
||||
}
|
||||
if (!is_null($this->database->postgres_conf)) {
|
||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
||||
'type' => 'bind',
|
||||
'source' => $this->configuration_dir . '/custom-postgres.conf',
|
||||
'target' => '/etc/postgresql/postgresql.conf',
|
||||
'read_only' => true,
|
||||
];
|
||||
$docker_compose['services'][$container_name]['command'] = [
|
||||
'postgres',
|
||||
'-c',
|
||||
'config_file=/etc/postgresql/postgresql.conf',
|
||||
];
|
||||
}
|
||||
$docker_compose = Yaml::dump($docker_compose, 10);
|
||||
$docker_compose_base64 = base64_encode($docker_compose);
|
||||
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
|
||||
$readme = generate_readme_file($this->database->name, now());
|
||||
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
||||
$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);
|
||||
}
|
||||
|
||||
@@ -171,4 +199,14 @@ class StartPostgresql
|
||||
$this->init_scripts[] = "$this->configuration_dir/docker-entrypoint-initdb.d/{$filename}";
|
||||
}
|
||||
}
|
||||
private function add_custom_conf()
|
||||
{
|
||||
if (is_null($this->database->postgres_conf)) {
|
||||
return;
|
||||
}
|
||||
$filename = 'custom-postgres.conf';
|
||||
$content = $this->database->postgres_conf;
|
||||
$content_base64 = base64_encode($content);
|
||||
$this->commands[] = "echo '{$content_base64}' | base64 -d > $this->configuration_dir/{$filename}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
];
|
||||
|
||||
@@ -65,7 +65,7 @@ class StartRedis
|
||||
'memswap_limit' => $this->database->limits_memory_swap,
|
||||
'mem_swappiness' => $this->database->limits_memory_swappiness,
|
||||
'mem_reservation' => $this->database->limits_memory_reservation,
|
||||
'cpus' => $this->database->limits_cpus,
|
||||
'cpus' => (int) $this->database->limits_cpus,
|
||||
'cpuset' => $this->database->limits_cpuset,
|
||||
'cpu_shares' => $this->database->limits_cpu_shares,
|
||||
]
|
||||
@@ -78,6 +78,16 @@ class StartRedis
|
||||
]
|
||||
]
|
||||
];
|
||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||
$docker_compose['services'][$container_name]['logging'] = [
|
||||
'driver' => 'fluentd',
|
||||
'options' => [
|
||||
'fluentd-address' => "tcp://127.0.0.1:24224",
|
||||
'fluentd-async' => "true",
|
||||
'fluentd-sub-second-precision' => "true",
|
||||
]
|
||||
];
|
||||
}
|
||||
if (count($this->database->ports_mappings_array) > 0) {
|
||||
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
|
||||
}
|
||||
@@ -101,8 +111,10 @@ class StartRedis
|
||||
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
|
||||
$readme = generate_readme_file($this->database->name, now());
|
||||
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
||||
$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);
|
||||
}
|
||||
|
||||
@@ -154,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}";
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Actions\Database;
|
||||
|
||||
use App\Models\ServiceDatabase;
|
||||
use App\Models\StandaloneMariadb;
|
||||
use App\Models\StandaloneMongodb;
|
||||
use App\Models\StandaloneMysql;
|
||||
@@ -13,9 +14,13 @@ class StopDatabaseProxy
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $database)
|
||||
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|ServiceDatabase $database)
|
||||
{
|
||||
instant_remote_process(["docker rm -f {$database->uuid}-proxy"], $database->destination->server);
|
||||
$server = data_get($database, 'destination.server');
|
||||
if ($database->getMorphClass() === 'App\Models\ServiceDatabase') {
|
||||
$server = data_get($database, 'service.server');
|
||||
}
|
||||
instant_remote_process(["docker rm -f {$database->uuid}-proxy"], $server);
|
||||
$database->is_public = false;
|
||||
$database->save();
|
||||
}
|
||||
|
||||
@@ -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: ' . $supported_os_type);
|
||||
$dockerVersion = '24.0';
|
||||
$config = base64_encode('{
|
||||
"log-driver": "json-file",
|
||||
@@ -27,36 +32,49 @@ 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...'",
|
||||
if ($supported_os_type === 'debian') {
|
||||
$command = $command->merge([
|
||||
"echo 'Installing Prerequisites...'",
|
||||
"command -v jq >/dev/null || apt-get update",
|
||||
"command -v jq >/dev/null || apt install -y jq",
|
||||
|
||||
]);
|
||||
} else if ($supported_os_type === 'rhel') {
|
||||
$command = $command->merge([
|
||||
"echo 'Installing Prerequisites...'",
|
||||
"command -v jq >/dev/null || dnf install -y jq",
|
||||
]);
|
||||
} else {
|
||||
throw new \Exception('Unsupported OS');
|
||||
}
|
||||
$command = $command->merge([
|
||||
"echo 'Installing Docker Engine...'",
|
||||
"curl https://releases.rancher.com/install-docker/{$dockerVersion}.sh | sh",
|
||||
"echo '####### Configuring Docker Engine (merging existing configuration with the required)...'",
|
||||
"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)...'",
|
||||
"echo 'Creating default Docker network (coolify)...'",
|
||||
"docker network create --attachable coolify >/dev/null 2>&1 || true",
|
||||
"echo '####### Done!'"
|
||||
];
|
||||
"echo 'Done!'"
|
||||
]);
|
||||
return remote_process($command, $server);
|
||||
}
|
||||
return remote_process($command, $server);
|
||||
}
|
||||
}
|
||||
|
||||
195
app/Actions/Server/InstallLogDrain.php
Normal file
195
app/Actions/Server/InstallLogDrain.php
Normal file
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Server;
|
||||
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use App\Models\Server;
|
||||
|
||||
class InstallLogDrain
|
||||
{
|
||||
use AsAction;
|
||||
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 {
|
||||
$type = 'none';
|
||||
}
|
||||
try {
|
||||
if ($type === 'none') {
|
||||
$command = [
|
||||
"echo 'Stopping old Fluent Bit'",
|
||||
"docker rm -f coolify-log-drain || true",
|
||||
];
|
||||
return instant_remote_process($command, $server);
|
||||
} else if ($type === 'newrelic') {
|
||||
if (!$server->settings->is_logdrain_newrelic_enabled) {
|
||||
throw new \Exception('New Relic log drain is not enabled.');
|
||||
}
|
||||
$config = base64_encode("
|
||||
[SERVICE]
|
||||
Flush 5
|
||||
Daemon off
|
||||
Tag container_logs
|
||||
Log_Level debug
|
||||
Parsers_File parsers.conf
|
||||
[INPUT]
|
||||
Name forward
|
||||
Buffer_Chunk_Size 1M
|
||||
Buffer_Max_Size 6M
|
||||
[FILTER]
|
||||
Name grep
|
||||
Match *
|
||||
Exclude log 127.0.0.1
|
||||
[FILTER]
|
||||
Name modify
|
||||
Match *
|
||||
Set server_name {$server->name}
|
||||
[OUTPUT]
|
||||
Name nrlogs
|
||||
Match *
|
||||
license_key \${LICENSE_KEY}
|
||||
# https://log-api.eu.newrelic.com/log/v1 - EU
|
||||
# https://log-api.newrelic.com/log/v1 - US
|
||||
base_uri \${BASE_URI}
|
||||
");
|
||||
} else if ($type === 'highlight') {
|
||||
if (!$server->settings->is_logdrain_highlight_enabled) {
|
||||
throw new \Exception('Highlight log drain is not enabled.');
|
||||
}
|
||||
$config = base64_encode("
|
||||
[SERVICE]
|
||||
Flush 5
|
||||
Daemon off
|
||||
Log_Level debug
|
||||
Parsers_File parsers.conf
|
||||
[INPUT]
|
||||
Name forward
|
||||
tag \${HIGHLIGHT_PROJECT_ID}
|
||||
Buffer_Chunk_Size 1M
|
||||
Buffer_Max_Size 6M
|
||||
[OUTPUT]
|
||||
Name forward
|
||||
Match *
|
||||
Host otel.highlight.io
|
||||
Port 24224
|
||||
");
|
||||
} else if ($type === 'axiom') {
|
||||
if (!$server->settings->is_logdrain_axiom_enabled) {
|
||||
throw new \Exception('Axiom log drain is not enabled.');
|
||||
}
|
||||
$config = base64_encode("
|
||||
[SERVICE]
|
||||
Flush 5
|
||||
Daemon off
|
||||
Log_Level debug
|
||||
Parsers_File parsers.conf
|
||||
[INPUT]
|
||||
Name forward
|
||||
Buffer_Chunk_Size 1M
|
||||
Buffer_Max_Size 6M
|
||||
[FILTER]
|
||||
Name grep
|
||||
Match *
|
||||
Exclude log 127.0.0.1
|
||||
[FILTER]
|
||||
Name modify
|
||||
Match *
|
||||
Set server_name {$server->name}
|
||||
[OUTPUT]
|
||||
Name http
|
||||
Match *
|
||||
Host api.axiom.co
|
||||
Port 443
|
||||
URI /v1/datasets/\${AXIOM_DATASET_NAME}/ingest
|
||||
# Authorization Bearer should be an API token
|
||||
Header Authorization Bearer \${AXIOM_API_KEY}
|
||||
compress gzip
|
||||
format json
|
||||
json_date_key _time
|
||||
json_date_format iso8601
|
||||
tls On
|
||||
");
|
||||
} else {
|
||||
throw new \Exception('Unknown log drain type.');
|
||||
}
|
||||
$parsers = base64_encode("
|
||||
[PARSER]
|
||||
Name empty_line_skipper
|
||||
Format regex
|
||||
Regex /^(?!\s*$).+/
|
||||
");
|
||||
$compose = base64_encode("
|
||||
services:
|
||||
coolify-log-drain:
|
||||
image: cr.fluentbit.io/fluent/fluent-bit:2.0
|
||||
container_name: coolify-log-drain
|
||||
command: -c /fluent-bit.conf
|
||||
env_file:
|
||||
- .env
|
||||
volumes:
|
||||
- ./fluent-bit.conf:/fluent-bit.conf
|
||||
- ./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.
|
||||
|
||||
Files:
|
||||
- `fluent-bit.conf` - configuration file for Fluent Bit
|
||||
- `docker-compose.yml` - docker-compose file to run Fluent Bit
|
||||
- `.env` - environment variables for Fluent Bit
|
||||
');
|
||||
$license_key = $server->settings->logdrain_newrelic_license_key;
|
||||
$base_uri = $server->settings->logdrain_newrelic_base_uri;
|
||||
$base_path = config('coolify.base_config_path');
|
||||
|
||||
$config_path = $base_path . '/log-drains';
|
||||
$fluent_bit_config = $config_path . '/fluent-bit.conf';
|
||||
$parsers_config = $config_path . '/parsers.conf';
|
||||
$compose_path = $config_path . '/docker-compose.yml';
|
||||
$readme_path = $config_path . '/README.md';
|
||||
$command = [
|
||||
"echo 'Saving configuration'",
|
||||
"mkdir -p $config_path",
|
||||
"echo '{$parsers}' | base64 -d > $parsers_config",
|
||||
"echo '{$config}' | base64 -d > $fluent_bit_config",
|
||||
"echo '{$compose}' | base64 -d > $compose_path",
|
||||
"echo '{$readme}' | base64 -d > $readme_path",
|
||||
"test -f $config_path/.env && rm $config_path/.env",
|
||||
|
||||
];
|
||||
if ($type === 'newrelic') {
|
||||
$add_envs_command = [
|
||||
"echo LICENSE_KEY=$license_key >> $config_path/.env",
|
||||
"echo BASE_URI=$base_uri >> $config_path/.env",
|
||||
];
|
||||
} else if ($type === 'highlight') {
|
||||
$add_envs_command = [
|
||||
"echo HIGHLIGHT_PROJECT_ID={$server->settings->logdrain_highlight_project_id} >> $config_path/.env",
|
||||
];
|
||||
} else if ($type === 'axiom') {
|
||||
$add_envs_command = [
|
||||
"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",
|
||||
];
|
||||
}
|
||||
$restart_command = [
|
||||
"echo 'Stopping old Fluent Bit'",
|
||||
"cd $config_path && docker rm -f coolify-log-drain || true",
|
||||
"echo 'Starting Fluent Bit'",
|
||||
"cd $config_path && docker compose up -d --remove-orphans",
|
||||
];
|
||||
$command = array_merge($command, $add_envs_command, $restart_command);
|
||||
return instant_remote_process($command, $server);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,19 +14,19 @@ 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 2>/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 || true";
|
||||
$commands[] = "echo 'Starting service {$service->name} on {$service->server->name}.'";
|
||||
$commands[] = "echo 'Pulling images.'";
|
||||
$commands[] = "docker compose pull";
|
||||
$commands[] = "echo '####### Starting containers.'";
|
||||
$commands[] = "echo 'Starting containers.'";
|
||||
$commands[] = "docker compose up -d --remove-orphans --force-recreate";
|
||||
$commands[] = "docker network connect $service->uuid coolify-proxy 2>/dev/null || true";
|
||||
$commands[] = "docker network connect $service->uuid coolify-proxy || true";
|
||||
$compose = data_get($service,'docker_compose',[]);
|
||||
$serviceNames = data_get(Yaml::parse($compose),'services',[]);
|
||||
foreach($serviceNames as $serviceName => $serviceConfig){
|
||||
$commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} $network {$serviceName}-{$service->uuid} 2>/dev/null || true";
|
||||
$commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} $network {$serviceName}-{$service->uuid} || true";
|
||||
}
|
||||
$activity = remote_process($commands, $service->server);
|
||||
return $activity;
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Enums\ApplicationDeploymentStatus;
|
||||
use App\Jobs\CleanupHelperContainersJob;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Models\ServiceDatabase;
|
||||
@@ -14,6 +17,7 @@ use App\Models\StandaloneMysql;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\StandaloneRedis;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class Init extends Command
|
||||
@@ -23,15 +27,41 @@ class Init extends Command
|
||||
|
||||
public function handle()
|
||||
{
|
||||
ray()->clearAll();
|
||||
$this->alive();
|
||||
$cleanup = $this->option('cleanup');
|
||||
if ($cleanup) {
|
||||
$this->cleanup_stucked_resources();
|
||||
$this->cleanup_ssh();
|
||||
}
|
||||
$this->cleanup_in_progress_application_deployments();
|
||||
$this->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) {
|
||||
echo "Skipping alive as do_not_track is enabled\n";
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Http::get("https://get.coollabs.io/coolify/v4/alive?appId=$id&version=$version");
|
||||
echo "I am alive!\n";
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in alive: {$e->getMessage()}\n";
|
||||
}
|
||||
}
|
||||
private function cleanup_ssh()
|
||||
{
|
||||
try {
|
||||
|
||||
41
app/Console/Commands/RootChangeEmail.php
Normal file
41
app/Console/Commands/RootChangeEmail.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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') {
|
||||
@@ -2,16 +2,17 @@
|
||||
|
||||
namespace App\Console;
|
||||
|
||||
use App\Jobs\CheckResaleLicenseJob;
|
||||
use App\Jobs\CheckLogDrainContainerJob;
|
||||
use App\Jobs\CleanupInstanceStuffsJob;
|
||||
use App\Jobs\DatabaseBackupJob;
|
||||
use App\Jobs\DockerCleanupJob;
|
||||
use App\Jobs\InstanceAutoUpdateJob;
|
||||
use App\Jobs\ContainerStatusJob;
|
||||
use App\Jobs\PullHelperImageJob;
|
||||
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;
|
||||
|
||||
@@ -27,7 +28,6 @@ class Kernel extends ConsoleKernel
|
||||
// Server Jobs
|
||||
$this->check_scheduled_backups($schedule);
|
||||
$this->check_resources($schedule);
|
||||
$this->cleanup_servers($schedule);
|
||||
$this->check_scheduled_backups($schedule);
|
||||
$this->pull_helper_image($schedule);
|
||||
} else {
|
||||
@@ -40,33 +40,31 @@ class Kernel extends ConsoleKernel
|
||||
$this->instance_auto_update($schedule);
|
||||
$this->check_scheduled_backups($schedule);
|
||||
$this->check_resources($schedule);
|
||||
$this->cleanup_servers($schedule);
|
||||
$this->pull_helper_image($schedule);
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
private function cleanup_servers($schedule)
|
||||
{
|
||||
$servers = Server::all()->where('settings.is_usable', true)->where('settings.is_reachable', true);
|
||||
foreach ($servers as $server) {
|
||||
$schedule->job(new DockerCleanupJob($server))->everyTenMinutes()->onOneServer();
|
||||
}
|
||||
}
|
||||
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)
|
||||
|
||||
@@ -4,7 +4,9 @@ namespace App\Exceptions;
|
||||
|
||||
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;
|
||||
@@ -40,6 +42,13 @@ class Handler extends ExceptionHandler
|
||||
];
|
||||
private InstanceSettings $settings;
|
||||
|
||||
protected function unauthenticated($request, AuthenticationException $exception)
|
||||
{
|
||||
if ($request->is('api/*') || $request->expectsJson() || $this->shouldReturnJson($request, $exception)) {
|
||||
return response()->json(['message' => $exception->getMessage()], 401);
|
||||
}
|
||||
return redirect()->guest($exception->redirectTo() ?? route('login'));
|
||||
}
|
||||
/**
|
||||
* Register the exception handling callbacks for the application.
|
||||
*/
|
||||
@@ -47,6 +56,9 @@ class Handler extends ExceptionHandler
|
||||
{
|
||||
$this->reportable(function (Throwable $e) {
|
||||
if (isDev()) {
|
||||
// return;
|
||||
}
|
||||
if ($e instanceof RuntimeException) {
|
||||
return;
|
||||
}
|
||||
$this->settings = InstanceSettings::get();
|
||||
@@ -65,6 +77,7 @@ class Handler extends ExceptionHandler
|
||||
);
|
||||
}
|
||||
);
|
||||
ray('reporting to sentry');
|
||||
Integration::captureUnhandledException($e);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -33,7 +33,7 @@ class Index extends Component
|
||||
public ?string $remoteServerUser = 'root';
|
||||
public ?Server $createdServer = null;
|
||||
|
||||
public Collection|array $projects = [];
|
||||
public Collection $projects;
|
||||
public ?int $selectedExistingProject = null;
|
||||
public ?Project $createdProject = null;
|
||||
|
||||
@@ -188,7 +188,6 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
public function validateServer()
|
||||
{
|
||||
try {
|
||||
$customErrorMessage = "Server is not reachable:";
|
||||
config()->set('coolify.mux_enabled', false);
|
||||
|
||||
instant_remote_process(['uptime'], $this->createdServer, true);
|
||||
@@ -198,7 +197,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
$this->serverReachable = false;
|
||||
return handleError(error: $e, customErrorMessage: $customErrorMessage, livewire: $this);
|
||||
return handleError(error: $e, livewire: $this);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -206,7 +205,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 +213,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()
|
||||
{
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Http\Livewire;
|
||||
|
||||
use App\Models\Project;
|
||||
use App\Models\S3Storage;
|
||||
use App\Models\Server;
|
||||
use Livewire\Component;
|
||||
|
||||
|
||||
54
app/Http/Livewire/Project/Application/Advanced.php
Normal file
54
app/Http/Livewire/Project/Application/Advanced.php
Normal 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');
|
||||
}
|
||||
}
|
||||
39
app/Http/Livewire/Project/Application/Configuration.php
Normal file
39
app/Http/Livewire/Project/Application/Configuration.php
Normal 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');
|
||||
}
|
||||
}
|
||||
@@ -26,14 +26,10 @@ class General extends Component
|
||||
public bool $isConfigurationChanged = false;
|
||||
|
||||
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;
|
||||
|
||||
|
||||
protected $listeners = [
|
||||
'resetDefaultLabels'
|
||||
];
|
||||
protected $rules = [
|
||||
'application.name' => 'required',
|
||||
'application.description' => 'nullable',
|
||||
@@ -55,6 +51,8 @@ class General extends Component
|
||||
'application.docker_registry_image_tag' => 'nullable',
|
||||
'application.dockerfile_location' => 'nullable',
|
||||
'application.custom_labels' => 'nullable',
|
||||
'application.dockerfile_target_build' => 'nullable',
|
||||
'application.settings.is_static' => 'boolean|required',
|
||||
];
|
||||
protected $validationAttributes = [
|
||||
'application.name' => 'name',
|
||||
@@ -77,6 +75,8 @@ class General extends Component
|
||||
'application.docker_registry_image_tag' => 'Docker registry image tag',
|
||||
'application.dockerfile_location' => 'Dockerfile location',
|
||||
'application.custom_labels' => 'Custom labels',
|
||||
'application.dockerfile_target_build' => 'Dockerfile target build',
|
||||
'application.settings.is_static' => 'Is static',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
@@ -91,17 +91,13 @@ 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->checkLabelUpdates();
|
||||
}
|
||||
public function instantSave()
|
||||
{
|
||||
$this->application->settings->save();
|
||||
$this->emit('success', 'Settings saved.');
|
||||
}
|
||||
public function updatedApplicationBuildPack()
|
||||
{
|
||||
if ($this->application->build_pack !== 'nixpacks') {
|
||||
@@ -118,32 +114,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()
|
||||
{
|
||||
|
||||
@@ -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,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);
|
||||
}
|
||||
|
||||
@@ -104,6 +104,8 @@ class CloneProject extends Component
|
||||
$uuid = (string)new Cuid2(7);
|
||||
$newDatabase = $database->replicate()->fill([
|
||||
'uuid' => $uuid,
|
||||
'status' => 'exited',
|
||||
'started_at' => null,
|
||||
'environment_id' => $newEnvironment->id,
|
||||
'destination_id' => $this->selectedServer,
|
||||
]);
|
||||
@@ -111,15 +113,15 @@ class CloneProject extends Component
|
||||
$environmentVaribles = $database->environment_variables()->get();
|
||||
foreach ($environmentVaribles as $environmentVarible) {
|
||||
$payload = [];
|
||||
if ($database->type() === 'standalone-postgres') {
|
||||
if ($database->type() === 'standalone-postgresql') {
|
||||
$payload['standalone_postgresql_id'] = $newDatabase->id;
|
||||
} else if ($database->type() === 'standalone_redis') {
|
||||
} else if ($database->type() === 'standalone-redis') {
|
||||
$payload['standalone_redis_id'] = $newDatabase->id;
|
||||
} else if ($database->type() === 'standalone_mongodb') {
|
||||
} else if ($database->type() === 'standalone-mongodb') {
|
||||
$payload['standalone_mongodb_id'] = $newDatabase->id;
|
||||
} else if ($database->type() === 'standalone_mysql') {
|
||||
} else if ($database->type() === 'standalone-mysql') {
|
||||
$payload['standalone_mysql_id'] = $newDatabase->id;
|
||||
}else if ($database->type() === 'standalone_mariadb') {
|
||||
} else if ($database->type() === 'standalone-mariadb') {
|
||||
$payload['standalone_mariadb_id'] = $newDatabase->id;
|
||||
}
|
||||
$newEnvironmentVariable = $environmentVarible->replicate()->fill($payload);
|
||||
@@ -134,6 +136,16 @@ class CloneProject extends Component
|
||||
'destination_id' => $this->selectedServer,
|
||||
]);
|
||||
$newService->save();
|
||||
foreach ($newService->applications() as $application) {
|
||||
$application->update([
|
||||
'status' => 'exited',
|
||||
]);
|
||||
}
|
||||
foreach ($newService->databases() as $database) {
|
||||
$database->update([
|
||||
'status' => 'exited',
|
||||
]);
|
||||
}
|
||||
$newService->parse();
|
||||
}
|
||||
return redirect()->route('project.resources', [
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Livewire\Project\Database;
|
||||
|
||||
use Livewire\Component;
|
||||
use Spatie\Url\Url;
|
||||
|
||||
class BackupEdit extends Component
|
||||
{
|
||||
@@ -43,14 +44,23 @@ class BackupEdit extends Component
|
||||
{
|
||||
// TODO: Delete backup from server and add a confirmation modal
|
||||
$this->backup->delete();
|
||||
redirect()->route('project.database.backups.all', $this->parameters);
|
||||
if ($this->backup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
|
||||
$previousUrl = url()->previous();
|
||||
$url = Url::fromString($previousUrl);
|
||||
$url = $url->withoutQueryParameter('selectedBackupId');
|
||||
$url = $url->withFragment('backups');
|
||||
$url = $url->getPath() . "#{$url->getFragment()}";
|
||||
return redirect()->to($url);
|
||||
} else {
|
||||
redirect()->route('project.database.backups.all', $this->parameters);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
$this->custom_validate();
|
||||
|
||||
$this->backup->save();
|
||||
$this->backup->refresh();
|
||||
$this->emit('success', 'Backup updated successfully');
|
||||
|
||||
@@ -19,7 +19,11 @@ class BackupExecutions extends Component
|
||||
$this->emit('error', 'Backup execution not found.');
|
||||
return;
|
||||
}
|
||||
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->destination->server);
|
||||
if ($execution->scheduledDatabaseBackup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
|
||||
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->service->destination->server);
|
||||
} else {
|
||||
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->destination->server);
|
||||
}
|
||||
$execution->delete();
|
||||
$this->emit('success', 'Backup deleted successfully.');
|
||||
$this->emit('refreshBackupExecutions');
|
||||
@@ -33,7 +37,11 @@ class BackupExecutions extends Component
|
||||
return;
|
||||
}
|
||||
$filename = data_get($execution, 'filename');
|
||||
$server = $execution->scheduledDatabaseBackup->database->destination->server;
|
||||
if ($execution->scheduledDatabaseBackup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
|
||||
$server = $execution->scheduledDatabaseBackup->database->service->destination->server;
|
||||
} else {
|
||||
$server = $execution->scheduledDatabaseBackup->database->destination->server;
|
||||
}
|
||||
$privateKeyLocation = savePrivateKeyToFs($server);
|
||||
$disk = Storage::build([
|
||||
'driver' => 'sftp',
|
||||
|
||||
@@ -22,7 +22,8 @@ class CreateScheduledBackup extends Component
|
||||
'frequency' => 'Backup Frequency',
|
||||
'save_s3' => 'Save to S3',
|
||||
];
|
||||
public function mount() {
|
||||
public function mount()
|
||||
{
|
||||
if ($this->s3s->count() > 0) {
|
||||
$this->s3_storage_id = $this->s3s->first()->id;
|
||||
}
|
||||
@@ -50,11 +51,16 @@ class CreateScheduledBackup extends Component
|
||||
$payload['databases_to_backup'] = $this->database->postgres_db;
|
||||
} else if ($this->database->type() === 'standalone-mysql') {
|
||||
$payload['databases_to_backup'] = $this->database->mysql_database;
|
||||
}else if ($this->database->type() === 'standalone-mariadb') {
|
||||
} else if ($this->database->type() === 'standalone-mariadb') {
|
||||
$payload['databases_to_backup'] = $this->database->mariadb_database;
|
||||
}
|
||||
ScheduledDatabaseBackup::create($payload);
|
||||
$this->emit('refreshScheduledBackups');
|
||||
|
||||
$databaseBackup = ScheduledDatabaseBackup::create($payload);
|
||||
if ($this->database->getMorphClass() === 'App\Models\ServiceDatabase') {
|
||||
$this->emit('refreshScheduledBackups', $databaseBackup->id);
|
||||
} else {
|
||||
$this->emit('refreshScheduledBackups');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
handleError($e, $this);
|
||||
} finally {
|
||||
|
||||
@@ -13,7 +13,8 @@ class General extends Component
|
||||
protected $listeners = ['refresh'];
|
||||
|
||||
public StandaloneMariadb $database;
|
||||
public string $db_url;
|
||||
public ?string $db_url = null;
|
||||
public ?string $db_url_public = null;
|
||||
|
||||
protected $rules = [
|
||||
'database.name' => 'required',
|
||||
@@ -27,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',
|
||||
@@ -41,9 +43,34 @@ class General extends Component
|
||||
'database.is_public' => 'Is Public',
|
||||
'database.public_port' => 'Public Port',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->db_url = $this->database->getDbUrl(true);
|
||||
if ($this->database->is_public) {
|
||||
$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 {
|
||||
if (str($this->database->public_port)->isEmpty()) {
|
||||
$this->database->public_port = null;
|
||||
}
|
||||
$this->validate();
|
||||
$this->database->save();
|
||||
$this->emit('success', 'Database updated successfully.');
|
||||
@@ -66,12 +93,13 @@ class General extends Component
|
||||
return;
|
||||
}
|
||||
StartDatabaseProxy::run($this->database);
|
||||
$this->db_url_public = $this->database->getDbUrl();
|
||||
$this->emit('success', 'Database is now publicly accessible.');
|
||||
} else {
|
||||
StopDatabaseProxy::run($this->database);
|
||||
$this->db_url_public = null;
|
||||
$this->emit('success', 'Database is no longer publicly accessible.');
|
||||
}
|
||||
$this->db_url = $this->database->getDbUrl();
|
||||
$this->database->save();
|
||||
} catch (\Throwable $e) {
|
||||
$this->database->is_public = !$this->database->is_public;
|
||||
@@ -83,11 +111,6 @@ class General extends Component
|
||||
$this->database->refresh();
|
||||
}
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->db_url = $this->database->getDbUrl();
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.database.mariadb.general');
|
||||
|
||||
@@ -13,7 +13,8 @@ class General extends Component
|
||||
protected $listeners = ['refresh'];
|
||||
|
||||
public StandaloneMongodb $database;
|
||||
public string $db_url;
|
||||
public ?string $db_url = null;
|
||||
public ?string $db_url_public = null;
|
||||
|
||||
protected $rules = [
|
||||
'database.name' => 'required',
|
||||
@@ -26,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',
|
||||
@@ -39,13 +41,39 @@ class General extends Component
|
||||
'database.is_public' => 'Is Public',
|
||||
'database.public_port' => 'Public Port',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->db_url = $this->database->getDbUrl(true);
|
||||
if ($this->database->is_public) {
|
||||
$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 {
|
||||
$this->validate();
|
||||
if ($this->database->mongo_conf === "") {
|
||||
if (str($this->database->public_port)->isEmpty()) {
|
||||
$this->database->public_port = null;
|
||||
}
|
||||
if (str($this->database->mongo_conf)->isEmpty()) {
|
||||
$this->database->mongo_conf = null;
|
||||
}
|
||||
$this->validate();
|
||||
$this->database->save();
|
||||
$this->emit('success', 'Database updated successfully.');
|
||||
} catch (Exception $e) {
|
||||
@@ -67,12 +95,13 @@ class General extends Component
|
||||
return;
|
||||
}
|
||||
StartDatabaseProxy::run($this->database);
|
||||
$this->db_url_public = $this->database->getDbUrl();
|
||||
$this->emit('success', 'Database is now publicly accessible.');
|
||||
} else {
|
||||
StopDatabaseProxy::run($this->database);
|
||||
$this->db_url_public = null;
|
||||
$this->emit('success', 'Database is no longer publicly accessible.');
|
||||
}
|
||||
$this->db_url = $this->database->getDbUrl();
|
||||
$this->database->save();
|
||||
} catch (\Throwable $e) {
|
||||
$this->database->is_public = !$this->database->is_public;
|
||||
@@ -84,11 +113,6 @@ class General extends Component
|
||||
$this->database->refresh();
|
||||
}
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->db_url = $this->database->getDbUrl();
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.database.mongodb.general');
|
||||
|
||||
@@ -13,7 +13,8 @@ class General extends Component
|
||||
protected $listeners = ['refresh'];
|
||||
|
||||
public StandaloneMysql $database;
|
||||
public string $db_url;
|
||||
public ?string $db_url = null;
|
||||
public ?string $db_url_public = null;
|
||||
|
||||
protected $rules = [
|
||||
'database.name' => 'required',
|
||||
@@ -27,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',
|
||||
@@ -41,9 +43,35 @@ class General extends Component
|
||||
'database.is_public' => 'Is Public',
|
||||
'database.public_port' => 'Public Port',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->db_url = $this->database->getDbUrl(true);
|
||||
if ($this->database->is_public) {
|
||||
$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 {
|
||||
if (str($this->database->public_port)->isEmpty()) {
|
||||
$this->database->public_port = null;
|
||||
}
|
||||
$this->validate();
|
||||
$this->database->save();
|
||||
$this->emit('success', 'Database updated successfully.');
|
||||
@@ -66,12 +94,13 @@ class General extends Component
|
||||
return;
|
||||
}
|
||||
StartDatabaseProxy::run($this->database);
|
||||
$this->db_url_public = $this->database->getDbUrl();
|
||||
$this->emit('success', 'Database is now publicly accessible.');
|
||||
} else {
|
||||
StopDatabaseProxy::run($this->database);
|
||||
$this->db_url_public = null;
|
||||
$this->emit('success', 'Database is no longer publicly accessible.');
|
||||
}
|
||||
$this->db_url = $this->database->getDbUrl();
|
||||
$this->database->save();
|
||||
} catch (\Throwable $e) {
|
||||
$this->database->is_public = !$this->database->is_public;
|
||||
@@ -83,11 +112,6 @@ class General extends Component
|
||||
$this->database->refresh();
|
||||
}
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->db_url = $this->database->getDbUrl();
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.database.mysql.general');
|
||||
|
||||
@@ -15,7 +15,8 @@ class General extends Component
|
||||
public StandalonePostgresql $database;
|
||||
public string $new_filename;
|
||||
public string $new_content;
|
||||
public string $db_url;
|
||||
public ?string $db_url = null;
|
||||
public ?string $db_url_public = null;
|
||||
|
||||
protected $listeners = ['refresh', 'save_init_script', 'delete_init_script'];
|
||||
|
||||
@@ -27,11 +28,13 @@ class General extends Component
|
||||
'database.postgres_db' => 'required',
|
||||
'database.postgres_initdb_args' => 'nullable',
|
||||
'database.postgres_host_auth_method' => 'nullable',
|
||||
'database.postgres_conf' => 'nullable',
|
||||
'database.init_scripts' => 'nullable',
|
||||
'database.image' => 'required',
|
||||
'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',
|
||||
@@ -41,6 +44,7 @@ class General extends Component
|
||||
'database.postgres_db' => 'Postgres DB',
|
||||
'database.postgres_initdb_args' => 'Postgres Initdb Args',
|
||||
'database.postgres_host_auth_method' => 'Postgres Host Auth Method',
|
||||
'database.postgres_conf' => 'Postgres Configuration',
|
||||
'database.init_scripts' => 'Init Scripts',
|
||||
'database.image' => 'Image',
|
||||
'database.ports_mappings' => 'Port Mapping',
|
||||
@@ -49,7 +53,24 @@ class General extends Component
|
||||
];
|
||||
public function mount()
|
||||
{
|
||||
$this->db_url = $this->database->getDbUrl();
|
||||
$this->db_url = $this->database->getDbUrl(true);
|
||||
if ($this->database->is_public) {
|
||||
$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()
|
||||
{
|
||||
@@ -66,12 +87,13 @@ class General extends Component
|
||||
return;
|
||||
}
|
||||
StartDatabaseProxy::run($this->database);
|
||||
$this->db_url_public = $this->database->getDbUrl();
|
||||
$this->emit('success', 'Database is now publicly accessible.');
|
||||
} else {
|
||||
StopDatabaseProxy::run($this->database);
|
||||
$this->db_url_public = null;
|
||||
$this->emit('success', 'Database is no longer publicly accessible.');
|
||||
}
|
||||
$this->db_url = $this->database->getDbUrl();
|
||||
$this->database->save();
|
||||
} catch (\Throwable $e) {
|
||||
$this->database->is_public = !$this->database->is_public;
|
||||
@@ -91,7 +113,6 @@ class General extends Component
|
||||
$collection = collect($this->database->init_scripts);
|
||||
$found = $collection->firstWhere('filename', $script['filename']);
|
||||
if ($found) {
|
||||
ray($collection->filter(fn ($s) => $s['filename'] !== $script['filename'])->toArray());
|
||||
$this->database->init_scripts = $collection->filter(fn ($s) => $s['filename'] !== $script['filename'])->toArray();
|
||||
$this->database->save();
|
||||
$this->refresh();
|
||||
@@ -135,6 +156,9 @@ class General extends Component
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
if (str($this->database->public_port)->isEmpty()) {
|
||||
$this->database->public_port = null;
|
||||
}
|
||||
$this->validate();
|
||||
$this->database->save();
|
||||
$this->emit('success', 'Database updated successfully.');
|
||||
|
||||
@@ -13,7 +13,8 @@ class General extends Component
|
||||
protected $listeners = ['refresh'];
|
||||
|
||||
public StandaloneRedis $database;
|
||||
public string $db_url;
|
||||
public ?string $db_url = null;
|
||||
public ?string $db_url_public = null;
|
||||
|
||||
protected $rules = [
|
||||
'database.name' => 'required',
|
||||
@@ -24,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',
|
||||
@@ -35,6 +37,27 @@ class General extends Component
|
||||
'database.is_public' => 'Is Public',
|
||||
'database.public_port' => 'Public Port',
|
||||
];
|
||||
public function mount()
|
||||
{
|
||||
$this->db_url = $this->database->getDbUrl(true);
|
||||
if ($this->database->is_public) {
|
||||
$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 {
|
||||
@@ -63,12 +86,13 @@ class General extends Component
|
||||
return;
|
||||
}
|
||||
StartDatabaseProxy::run($this->database);
|
||||
$this->db_url_public = $this->database->getDbUrl();
|
||||
$this->emit('success', 'Database is now publicly accessible.');
|
||||
} else {
|
||||
StopDatabaseProxy::run($this->database);
|
||||
$this->db_url_public = null;
|
||||
$this->emit('success', 'Database is no longer publicly accessible.');
|
||||
}
|
||||
$this->db_url = $this->database->getDbUrl();
|
||||
$this->database->save();
|
||||
} catch (\Throwable $e) {
|
||||
$this->database->is_public = !$this->database->is_public;
|
||||
@@ -80,10 +104,6 @@ class General extends Component
|
||||
$this->database->refresh();
|
||||
}
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->db_url = $this->database->getDbUrl();
|
||||
}
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.database.redis.general');
|
||||
|
||||
@@ -8,13 +8,33 @@ class ScheduledBackups extends Component
|
||||
{
|
||||
public $database;
|
||||
public $parameters;
|
||||
public $type;
|
||||
public $selectedBackup;
|
||||
public $selectedBackupId;
|
||||
public $s3s;
|
||||
protected $listeners = ['refreshScheduledBackups'];
|
||||
protected $queryString = ['selectedBackupId'];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
if ($this->selectedBackupId) {
|
||||
$this->setSelectedBackup($this->selectedBackupId);
|
||||
}
|
||||
$this->parameters = get_route_parameters();
|
||||
if ($this->database->getMorphClass() === 'App\Models\ServiceDatabase') {
|
||||
$this->type = 'service-database';
|
||||
} else {
|
||||
$this->type = 'database';
|
||||
}
|
||||
$this->s3s = currentTeam()->s3s;
|
||||
}
|
||||
public function setSelectedBackup($backupId) {
|
||||
$this->selectedBackupId = $backupId;
|
||||
$this->selectedBackup = $this->database->scheduledBackups->find($this->selectedBackupId);
|
||||
if (is_null($this->selectedBackup)) {
|
||||
$this->selectedBackupId = null;
|
||||
}
|
||||
}
|
||||
|
||||
public function delete($scheduled_backup_id): void
|
||||
{
|
||||
$this->database->scheduledBackups->find($scheduled_backup_id)->delete();
|
||||
@@ -22,9 +42,11 @@ class ScheduledBackups extends Component
|
||||
$this->refreshScheduledBackups();
|
||||
}
|
||||
|
||||
public function refreshScheduledBackups(): void
|
||||
public function refreshScheduledBackups(?int $id = null): void
|
||||
{
|
||||
ray('refreshScheduledBackups');
|
||||
$this->database->refresh();
|
||||
if ($id) {
|
||||
$this->setSelectedBackup($id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,7 +26,16 @@ 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 {
|
||||
|
||||
@@ -2,28 +2,69 @@
|
||||
|
||||
namespace App\Http\Livewire\Project\Service;
|
||||
|
||||
use App\Actions\Database\StartDatabaseProxy;
|
||||
use App\Actions\Database\StopDatabaseProxy;
|
||||
use App\Models\ServiceDatabase;
|
||||
use Livewire\Component;
|
||||
|
||||
class Database extends Component
|
||||
{
|
||||
public ServiceDatabase $database;
|
||||
public ?string $db_url_public = null;
|
||||
public $fileStorages;
|
||||
|
||||
protected $listeners = ["refreshFileStorages"];
|
||||
protected $rules = [
|
||||
'database.human_name' => 'nullable',
|
||||
'database.description' => 'nullable',
|
||||
'database.image' => 'required',
|
||||
'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;
|
||||
return;
|
||||
}
|
||||
if ($this->database->is_public) {
|
||||
if (!str($this->database->status)->startsWith('running')) {
|
||||
$this->emit('error', 'Database must be started to be publicly accessible.');
|
||||
$this->database->is_public = false;
|
||||
return;
|
||||
}
|
||||
StartDatabaseProxy::run($this->database);
|
||||
$this->db_url_public = $this->database->getServiceDatabaseUrl();
|
||||
$this->emit('success', 'Database is now publicly accessible.');
|
||||
} else {
|
||||
StopDatabaseProxy::run($this->database);
|
||||
$this->db_url_public = null;
|
||||
$this->emit('success', 'Database is no longer publicly accessible.');
|
||||
}
|
||||
$this->submit();
|
||||
}
|
||||
public function refreshFileStorages()
|
||||
|
||||
@@ -27,11 +27,15 @@ class Navbar extends Component
|
||||
$activity = StartService::run($this->service);
|
||||
$this->emit('newMonitorActivity', $activity->id);
|
||||
}
|
||||
public function stop()
|
||||
public function stop(bool $forceCleanup = false)
|
||||
{
|
||||
StopService::run($this->service);
|
||||
$this->service->refresh();
|
||||
$this->emit('success', 'Service stopped successfully.');
|
||||
if ($forceCleanup) {
|
||||
$this->emit('success', 'Force cleanup service successfully.');
|
||||
} else {
|
||||
$this->emit('success', 'Service stopped successfully.');
|
||||
}
|
||||
$this->emit('checkStatus');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ class Show extends Component
|
||||
public array $parameters;
|
||||
public array $query;
|
||||
public Collection $services;
|
||||
public $s3s;
|
||||
|
||||
protected $listeners = ['generateDockerCompose'];
|
||||
|
||||
public function mount()
|
||||
@@ -33,6 +35,7 @@ class Show extends Component
|
||||
$this->serviceDatabase = $this->service->databases()->whereName($this->parameters['service_name'])->first();
|
||||
$this->serviceDatabase->getFilesFromServer();
|
||||
}
|
||||
$this->s3s = currentTeam()->s3s;
|
||||
} catch(\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -7,15 +7,39 @@ use Livewire\Component;
|
||||
class StackForm extends Component
|
||||
{
|
||||
public $service;
|
||||
public $fields = [];
|
||||
protected $listeners = ["saveCompose"];
|
||||
protected $rules = [
|
||||
public $rules = [
|
||||
'service.docker_compose_raw' => 'required',
|
||||
'service.docker_compose' => 'required',
|
||||
'service.name' => 'required',
|
||||
'service.description' => 'nullable',
|
||||
];
|
||||
public $validationAttributes = [];
|
||||
public function mount()
|
||||
{
|
||||
$extraFields = $this->service->extraFields();
|
||||
foreach ($extraFields as $serviceName => $fields) {
|
||||
foreach ($fields as $fieldKey => $field) {
|
||||
$key = data_get($field, 'key');
|
||||
$value = data_get($field, 'value');
|
||||
$rules = data_get($field, 'rules');
|
||||
$isPassword = data_get($field, 'isPassword');
|
||||
$this->fields[$key] = [
|
||||
"serviceName" => $serviceName,
|
||||
"key" => $key,
|
||||
"name" => $fieldKey,
|
||||
"value" => $value,
|
||||
"isPassword" => $isPassword,
|
||||
];
|
||||
$this->rules["fields.$key.value"] = $rules;
|
||||
$this->validationAttributes["fields.$key.value"] = $fieldKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
public function saveCompose($raw)
|
||||
{
|
||||
|
||||
$this->service->docker_compose_raw = $raw;
|
||||
$this->submit();
|
||||
}
|
||||
@@ -25,6 +49,7 @@ class StackForm extends Component
|
||||
try {
|
||||
$this->validate();
|
||||
$this->service->save();
|
||||
$this->service->saveExtraFields($this->fields);
|
||||
$this->service->parse();
|
||||
$this->service->refresh();
|
||||
$this->service->saveComposeConfigs();
|
||||
|
||||
@@ -6,5 +6,7 @@ use Livewire\Component;
|
||||
|
||||
class Destination extends Component
|
||||
{
|
||||
public $destination;
|
||||
public $resource;
|
||||
public $servers = [];
|
||||
public $additionalServers = [];
|
||||
}
|
||||
|
||||
@@ -55,12 +55,17 @@ class All extends Component
|
||||
{
|
||||
if ($isPreview) {
|
||||
$variables = parseEnvFormatToArray($this->variablesPreview);
|
||||
$this->resource->environment_variables_preview()->whereNotIn('key', array_keys($variables))->delete();
|
||||
} else {
|
||||
$variables = parseEnvFormatToArray($this->variables);
|
||||
$this->resource->environment_variables()->whereNotIn('key', array_keys($variables))->delete();
|
||||
}
|
||||
foreach ($variables as $key => $variable) {
|
||||
$found = $this->resource->environment_variables()->where('key', $key)->first();
|
||||
$foundPreview = $this->resource->environment_variables_preview()->where('key', $key)->first();
|
||||
if ($isPreview) {
|
||||
$found = $this->resource->environment_variables_preview()->where('key', $key)->first();
|
||||
} else {
|
||||
$found = $this->resource->environment_variables()->where('key', $key)->first();
|
||||
}
|
||||
if ($found) {
|
||||
if ($found->is_shown_once) {
|
||||
continue;
|
||||
@@ -68,14 +73,6 @@ class All extends Component
|
||||
$found->value = $variable;
|
||||
$found->save();
|
||||
continue;
|
||||
}
|
||||
if ($foundPreview) {
|
||||
if ($foundPreview->is_shown_once) {
|
||||
continue;
|
||||
}
|
||||
$foundPreview->value = $variable;
|
||||
$foundPreview->save();
|
||||
continue;
|
||||
} else {
|
||||
$environment = new EnvironmentVariable();
|
||||
$environment->key = $key;
|
||||
|
||||
@@ -17,7 +17,7 @@ class GetLogs extends Component
|
||||
public int $numberOfLines = 100;
|
||||
public function doSomethingWithThisChunkOfOutput($output)
|
||||
{
|
||||
$this->outputs .= $output;
|
||||
$this->outputs .= removeAnsiColors($output);
|
||||
}
|
||||
public function instantSave()
|
||||
{
|
||||
|
||||
@@ -31,7 +31,7 @@ class Logs extends Component
|
||||
$this->resource = Application::where('uuid', $this->parameters['application_uuid'])->firstOrFail();
|
||||
$this->status = $this->resource->status;
|
||||
$this->server = $this->resource->destination->server;
|
||||
$containers = getCurrentApplicationContainerStatus($this->server, $this->resource->id);
|
||||
$containers = getCurrentApplicationContainerStatus($this->server, $this->resource->id, 0);
|
||||
if ($containers->count() > 0) {
|
||||
$this->container = data_get($containers[0], 'Names');
|
||||
}
|
||||
|
||||
@@ -8,9 +8,27 @@ class Webhooks extends Component
|
||||
{
|
||||
public $resource;
|
||||
public ?string $deploywebhook = null;
|
||||
public ?string $githubManualWebhook = null;
|
||||
public ?string $gitlabManualWebhook = null;
|
||||
protected $rules = [
|
||||
'resource.manual_webhook_secret_github' => 'nullable|string',
|
||||
'resource.manual_webhook_secret_gitlab' => 'nullable|string',
|
||||
];
|
||||
public function saveSecret()
|
||||
{
|
||||
try {
|
||||
$this->validate();
|
||||
$this->resource->save();
|
||||
$this->emit('success','Secret Saved.');
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
public function mount()
|
||||
{
|
||||
$this->deploywebhook = generateDeployWebhook($this->resource);
|
||||
$this->githubManualWebhook = generateGitManualWebhook($this->resource, 'github');
|
||||
$this->gitlabManualWebhook = generateGitManualWebhook($this->resource, 'gitlab');
|
||||
}
|
||||
public function render()
|
||||
{
|
||||
|
||||
@@ -43,9 +43,9 @@ 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()
|
||||
{
|
||||
@@ -77,12 +77,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,7 +95,7 @@ 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;
|
||||
|
||||
133
app/Http/Livewire/Server/LogDrains.php
Normal file
133
app/Http/Livewire/Server/LogDrains.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire\Server;
|
||||
|
||||
use App\Actions\Server\InstallLogDrain;
|
||||
use App\Models\Server;
|
||||
use Livewire\Component;
|
||||
|
||||
class LogDrains extends Component
|
||||
{
|
||||
public Server $server;
|
||||
public $parameters = [];
|
||||
protected $rules = [
|
||||
'server.settings.is_logdrain_newrelic_enabled' => 'required|boolean',
|
||||
'server.settings.logdrain_newrelic_license_key' => 'required|string',
|
||||
'server.settings.logdrain_newrelic_base_uri' => 'required|string',
|
||||
'server.settings.is_logdrain_highlight_enabled' => 'required|boolean',
|
||||
'server.settings.logdrain_highlight_project_id' => 'required|string',
|
||||
'server.settings.is_logdrain_axiom_enabled' => 'required|boolean',
|
||||
'server.settings.logdrain_axiom_dataset_name' => 'required|string',
|
||||
'server.settings.logdrain_axiom_api_key' => 'required|string',
|
||||
];
|
||||
protected $validationAttributes = [
|
||||
'server.settings.is_logdrain_newrelic_enabled' => 'New Relic log drain',
|
||||
'server.settings.logdrain_newrelic_license_key' => 'New Relic license key',
|
||||
'server.settings.logdrain_newrelic_base_uri' => 'New Relic base URI',
|
||||
'server.settings.is_logdrain_highlight_enabled' => 'Highlight log drain',
|
||||
'server.settings.logdrain_highlight_project_id' => 'Highlight project ID',
|
||||
'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',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
try {
|
||||
$server = Server::ownedByCurrentTeam(['name', 'description', 'ip', 'port', 'user', 'proxy'])->whereUuid(request()->server_uuid)->first();
|
||||
if (is_null($server)) {
|
||||
return redirect()->route('server.all');
|
||||
}
|
||||
$this->server = $server;
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
public function configureLogDrain()
|
||||
{
|
||||
try {
|
||||
InstallLogDrain::run($this->server);
|
||||
if (!$this->server->isLogDrainEnabled()) {
|
||||
$this->emit('serverRefresh');
|
||||
$this->emit('success', 'Log drain service stopped.');
|
||||
return;
|
||||
}
|
||||
$this->emit('serverRefresh');
|
||||
$this->emit('success', 'Log drain service started successfully.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
public function instantSave(string $type)
|
||||
{
|
||||
try {
|
||||
$ok = $this->submit($type);
|
||||
if (!$ok) {
|
||||
return;
|
||||
}
|
||||
$this->configureLogDrain();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
public function submit(string $type)
|
||||
{
|
||||
try {
|
||||
$this->resetErrorBag();
|
||||
if ($type === 'newrelic') {
|
||||
$this->validate([
|
||||
'server.settings.is_logdrain_newrelic_enabled' => 'required|boolean',
|
||||
'server.settings.logdrain_newrelic_license_key' => 'required|string',
|
||||
'server.settings.logdrain_newrelic_base_uri' => 'required|string',
|
||||
]);
|
||||
$this->server->settings->update([
|
||||
'is_logdrain_highlight_enabled' => false,
|
||||
'is_logdrain_axiom_enabled' => false,
|
||||
]);
|
||||
} else if ($type === 'highlight') {
|
||||
$this->validate([
|
||||
'server.settings.is_logdrain_highlight_enabled' => 'required|boolean',
|
||||
'server.settings.logdrain_highlight_project_id' => 'required|string',
|
||||
]);
|
||||
$this->server->settings->update([
|
||||
'is_logdrain_newrelic_enabled' => false,
|
||||
'is_logdrain_axiom_enabled' => false,
|
||||
]);
|
||||
} else if ($type === 'axiom') {
|
||||
$this->validate([
|
||||
'server.settings.is_logdrain_axiom_enabled' => 'required|boolean',
|
||||
'server.settings.logdrain_axiom_dataset_name' => 'required|string',
|
||||
'server.settings.logdrain_axiom_api_key' => 'required|string',
|
||||
]);
|
||||
$this->server->settings->update([
|
||||
'is_logdrain_newrelic_enabled' => false,
|
||||
'is_logdrain_highlight_enabled' => false,
|
||||
]);
|
||||
}
|
||||
$this->server->settings->save();
|
||||
$this->emit('success', 'Settings saved successfully.');
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
if ($type === 'newrelic') {
|
||||
$this->server->settings->update([
|
||||
'is_logdrain_newrelic_enabled' => false,
|
||||
]);
|
||||
} else if ($type === 'highlight') {
|
||||
$this->server->settings->update([
|
||||
'is_logdrain_highlight_enabled' => false,
|
||||
]);
|
||||
} else if ($type === 'axiom') {
|
||||
$this->server->settings->update([
|
||||
'is_logdrain_axiom_enabled' => false,
|
||||
]);
|
||||
}
|
||||
handleError($e, $this);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.server.log-drains');
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ class Show extends Component
|
||||
}
|
||||
public function submit()
|
||||
{
|
||||
$this->emit('serverRefresh');
|
||||
$this->emit('serverRefresh',false);
|
||||
}
|
||||
public function render()
|
||||
{
|
||||
|
||||
@@ -69,7 +69,6 @@ class Backup extends Component
|
||||
]);
|
||||
$this->database->refresh();
|
||||
$this->backup->refresh();
|
||||
ray($this->backup);
|
||||
$this->s3s = S3Storage::whereTeamId(0)->get();
|
||||
}
|
||||
|
||||
|
||||
@@ -3,17 +3,18 @@
|
||||
namespace App\Http\Livewire\Source\Github;
|
||||
|
||||
use App\Models\GithubApp;
|
||||
use App\Models\InstanceSettings;
|
||||
use Livewire\Component;
|
||||
|
||||
class Change extends Component
|
||||
{
|
||||
public string $webhook_endpoint;
|
||||
public string|null $ipv4;
|
||||
public string|null $ipv6;
|
||||
public string|null $fqdn;
|
||||
public ?string $ipv4;
|
||||
public ?string $ipv6;
|
||||
public ?string $fqdn;
|
||||
|
||||
public bool|null $default_permissions = true;
|
||||
public bool|null $preview_deployment_permissions = true;
|
||||
public ?bool $default_permissions = true;
|
||||
public ?bool $preview_deployment_permissions = true;
|
||||
|
||||
public $parameters;
|
||||
public GithubApp $github_app;
|
||||
@@ -28,29 +29,68 @@ class Change extends Component
|
||||
'github_app.custom_user' => 'required|string',
|
||||
'github_app.custom_port' => 'required|int',
|
||||
'github_app.app_id' => 'required|int',
|
||||
'github_app.installation_id' => 'nullable',
|
||||
'github_app.client_id' => 'nullable',
|
||||
'github_app.client_secret' => 'nullable',
|
||||
'github_app.webhook_secret' => 'nullable',
|
||||
'github_app.installation_id' => 'required|int',
|
||||
'github_app.client_id' => 'required|string',
|
||||
'github_app.client_secret' => 'required|string',
|
||||
'github_app.webhook_secret' => 'required|string',
|
||||
'github_app.is_system_wide' => 'required|bool',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$github_app_uuid = request()->github_app_uuid;
|
||||
$this->github_app = GithubApp::where('uuid', $github_app_uuid)->first();
|
||||
if (!$this->github_app) {
|
||||
return redirect()->route('source.all');
|
||||
}
|
||||
$settings = InstanceSettings::get();
|
||||
$this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
|
||||
|
||||
$this->name = str($this->github_app->name)->kebab();
|
||||
$this->fqdn = $settings->fqdn;
|
||||
|
||||
if ($settings->public_ipv4) {
|
||||
$this->ipv4 = 'http://' . $settings->public_ipv4 . ':' . config('app.port');
|
||||
}
|
||||
if ($settings->public_ipv6) {
|
||||
$this->ipv6 = 'http://' . $settings->public_ipv6 . ':' . config('app.port');
|
||||
}
|
||||
if ($this->github_app->installation_id && session('from')) {
|
||||
$source_id = data_get(session('from'), 'source_id');
|
||||
if (!$source_id || $this->github_app->id !== $source_id) {
|
||||
session()->forget('from');
|
||||
} else {
|
||||
$parameters = data_get(session('from'), 'parameters');
|
||||
$back = data_get(session('from'), 'back');
|
||||
$environment_name = data_get($parameters, 'environment_name');
|
||||
$project_uuid = data_get($parameters, 'project_uuid');
|
||||
$type = data_get($parameters, 'type');
|
||||
$destination = data_get($parameters, 'destination');
|
||||
session()->forget('from');
|
||||
return redirect()->route($back, [
|
||||
'environment_name' => $environment_name,
|
||||
'project_uuid' => $project_uuid,
|
||||
'type' => $type,
|
||||
'destination' => $destination,
|
||||
]);
|
||||
}
|
||||
}
|
||||
$this->parameters = get_route_parameters();
|
||||
if (isCloud() && !isDev()) {
|
||||
$this->webhook_endpoint = config('app.url');
|
||||
} else {
|
||||
$this->webhook_endpoint = $this->ipv4;
|
||||
$this->is_system_wide = $this->github_app->is_system_wide;
|
||||
}
|
||||
$this->parameters = get_route_parameters();
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
$this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
|
||||
$this->validate();
|
||||
$this->github_app->save();
|
||||
$this->emit('success', 'Github App updated successfully.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
@@ -58,6 +98,7 @@ class Change extends Component
|
||||
|
||||
public function instantSave()
|
||||
{
|
||||
$this->submit();
|
||||
}
|
||||
|
||||
public function delete()
|
||||
|
||||
17
app/Http/Livewire/Sponsorship.php
Normal file
17
app/Http/Livewire/Sponsorship.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
class Sponsorship extends Component
|
||||
{
|
||||
public function disable()
|
||||
{
|
||||
auth()->user()->update(['is_notification_sponsorship_enabled' => false]);
|
||||
}
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.sponsorship');
|
||||
}
|
||||
}
|
||||
@@ -64,21 +64,10 @@ class Create extends Component
|
||||
}
|
||||
$this->storage->team_id = currentTeam()->id;
|
||||
$this->storage->testConnection();
|
||||
$this->storage->is_usable = true;
|
||||
$this->storage->save();
|
||||
return redirect()->route('team.storages.show', $this->storage->uuid);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
private function test_s3_connection()
|
||||
{
|
||||
try {
|
||||
$this->storage->testConnection();
|
||||
return $this->emit('success', 'Connection is working. Tested with "ListObjectsV2" action.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ class Form extends Component
|
||||
public function test_s3_connection()
|
||||
{
|
||||
try {
|
||||
$this->storage->testConnection();
|
||||
$this->storage->testConnection(shouldSave: true);
|
||||
return $this->emit('success', 'Connection is working. Tested with "ListObjectsV2" action.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
@@ -53,10 +53,7 @@ class Form extends Component
|
||||
{
|
||||
$this->validate();
|
||||
try {
|
||||
$this->storage->testConnection();
|
||||
$this->emit('success', 'Connection is working. Tested with "ListObjectsV2" action.');
|
||||
$this->storage->save();
|
||||
$this->emit('success', 'Storage settings saved.');
|
||||
$this->test_s3_connection();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
111
app/Jobs/ApplicationDeployDockerImageJob.php
Normal file
111
app/Jobs/ApplicationDeployDockerImageJob.php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Enums\ApplicationDeploymentStatus;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Traits\ExecuteRemoteCommandNew;
|
||||
use Exception;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Throwable;
|
||||
|
||||
class ApplicationDeployDockerImageJob implements ShouldQueue, ShouldBeEncrypted
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, ExecuteRemoteCommandNew;
|
||||
|
||||
public $timeout = 3600;
|
||||
public $tries = 1;
|
||||
public string $applicationDeploymentQueueId;
|
||||
|
||||
public function __construct(string $applicationDeploymentQueueId)
|
||||
{
|
||||
$this->applicationDeploymentQueueId = $applicationDeploymentQueueId;
|
||||
}
|
||||
public function handle()
|
||||
{
|
||||
ray()->clearAll();
|
||||
ray('Deploying Docker Image');
|
||||
try {
|
||||
$applicationDeploymentQueue = ApplicationDeploymentQueue::find($this->applicationDeploymentQueueId);
|
||||
$application = Application::find($applicationDeploymentQueue->application_id);
|
||||
|
||||
$deploymentUuid = data_get($applicationDeploymentQueue, 'deployment_uuid');
|
||||
$dockerImage = data_get($application, 'docker_registry_image_name');
|
||||
$dockerImageTag = data_get($application, 'docker_registry_image_tag');
|
||||
$productionImageName = str("{$dockerImage}:{$dockerImageTag}");
|
||||
$destination = $application->destination->getMorphClass()::where('id', $application->destination->id)->first();
|
||||
$pullRequestId = data_get($applicationDeploymentQueue, 'pull_request_id');
|
||||
|
||||
$server = data_get($destination, 'server');
|
||||
$network = data_get($destination, 'network');
|
||||
|
||||
$containerName = generateApplicationContainerName($application, $pullRequestId);
|
||||
savePrivateKeyToFs($server);
|
||||
|
||||
ray("echo 'Starting deployment of {$productionImageName}.'");
|
||||
|
||||
$applicationDeploymentQueue->update([
|
||||
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
|
||||
]);
|
||||
$this->executeRemoteCommand(
|
||||
server: $server,
|
||||
logModel: $applicationDeploymentQueue,
|
||||
commands: prepareHelperContainer($server, $network, $deploymentUuid)
|
||||
);
|
||||
|
||||
$this->executeRemoteCommand(
|
||||
server: $server,
|
||||
logModel: $applicationDeploymentQueue,
|
||||
commands: generateComposeFile(
|
||||
deploymentUuid: $deploymentUuid,
|
||||
server: $server,
|
||||
network: $network,
|
||||
application: $application,
|
||||
containerName: $containerName,
|
||||
imageName: $productionImageName,
|
||||
pullRequestId: $pullRequestId
|
||||
)
|
||||
);
|
||||
$this->executeRemoteCommand(
|
||||
server: $server,
|
||||
logModel: $applicationDeploymentQueue,
|
||||
commands: rollingUpdate(application: $application, deploymentUuid: $deploymentUuid)
|
||||
);
|
||||
} catch (Throwable $e) {
|
||||
$this->executeRemoteCommand(
|
||||
server: $server,
|
||||
logModel: $applicationDeploymentQueue,
|
||||
commands: [
|
||||
"echo 'Oops something is not okay, are you okay? 😢'",
|
||||
"echo '{$e->getMessage()}'",
|
||||
"echo -n 'Deployment failed. Removing the new version of your application.'",
|
||||
executeInDocker($deploymentUuid, "docker rm -f $containerName >/dev/null 2>&1"),
|
||||
]
|
||||
);
|
||||
// $this->next(ApplicationDeploymentStatus::FAILED->value);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
// private function next(string $status)
|
||||
// {
|
||||
// // If the deployment is cancelled by the user, don't update the status
|
||||
// if ($this->application_deployment_queue->status !== ApplicationDeploymentStatus::CANCELLED_BY_USER->value) {
|
||||
// $this->application_deployment_queue->update([
|
||||
// 'status' => $status,
|
||||
// ]);
|
||||
// }
|
||||
// queue_next_deployment($this->application);
|
||||
// if ($status === ApplicationDeploymentStatus::FINISHED->value) {
|
||||
// $this->application->environment->project->team->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview));
|
||||
// }
|
||||
// if ($status === ApplicationDeploymentStatus::FAILED->value) {
|
||||
// $this->application->environment->project->team->notify(new DeploymentFailed($this->application, $this->deployment_uuid, $this->preview));
|
||||
// }
|
||||
// }
|
||||
}
|
||||
29
app/Jobs/ApplicationDeploySimpleDockerfileJob.php
Normal file
29
app/Jobs/ApplicationDeploySimpleDockerfileJob.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?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 ApplicationDeploySimpleDockerfileJob 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('Deploying Simple Dockerfile');
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -33,6 +34,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, ExecuteRemoteCommand;
|
||||
|
||||
public $timeout = 3600;
|
||||
|
||||
public static int $batch_counter = 0;
|
||||
|
||||
private int $application_deployment_queue_id;
|
||||
@@ -52,7 +55,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
private GithubApp|GitlabApp|string $source = 'other';
|
||||
private StandaloneDocker|SwarmDocker $destination;
|
||||
private Server $server;
|
||||
private ApplicationPreview|null $preview = null;
|
||||
private Server $mainServer;
|
||||
private ?ApplicationPreview $preview = null;
|
||||
private ?string $git_type = null;
|
||||
|
||||
private string $container_name;
|
||||
private ?string $currently_running_container_name = null;
|
||||
@@ -69,14 +74,17 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
private $docker_compose_base64;
|
||||
private string $dockerfile_location = '/Dockerfile';
|
||||
private ?string $addHosts = null;
|
||||
private ?string $buildTarget = null;
|
||||
private $log_model;
|
||||
private Collection $saved_outputs;
|
||||
private ?string $full_healthcheck_url = null;
|
||||
|
||||
private string $serverUser = 'root';
|
||||
private string $serverUserHomeDir = '/root';
|
||||
private string $dockerConfigFileExists = 'NOK';
|
||||
|
||||
private int $customPort = 22;
|
||||
private ?string $customRepository = null;
|
||||
|
||||
private ?string $fullRepoUrl = null;
|
||||
private ?string $branch = null;
|
||||
@@ -97,12 +105,14 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
$this->force_rebuild = $this->application_deployment_queue->force_rebuild;
|
||||
$this->restart_only = $this->application_deployment_queue->restart_only;
|
||||
|
||||
$this->git_type = data_get($this->application_deployment_queue, 'git_type');
|
||||
|
||||
$source = data_get($this->application, 'source');
|
||||
if ($source) {
|
||||
$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->workdir = "{$this->basedir}" . rtrim($this->application->base_directory, '/');
|
||||
@@ -117,11 +127,16 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
if ($this->pull_request_id !== 0) {
|
||||
$this->preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->application->id, $this->pull_request_id);
|
||||
if ($this->application->fqdn) {
|
||||
if (data_get($this->preview, 'fqdn')) {
|
||||
$preview_fqdn = getFqdnWithoutPort(data_get($this->preview, 'fqdn'));
|
||||
if (str($this->application->fqdn)->contains(',')) {
|
||||
$url = Url::fromString(str($this->application->fqdn)->explode(',')[0]);
|
||||
$preview_fqdn = getFqdnWithoutPort(str($this->application->fqdn)->explode(',')[0]);
|
||||
} else {
|
||||
$url = Url::fromString($this->application->fqdn);
|
||||
if (data_get($this->preview, 'fqdn')) {
|
||||
$preview_fqdn = getFqdnWithoutPort(data_get($this->preview, 'fqdn'));
|
||||
}
|
||||
}
|
||||
$template = $this->application->preview_url_template;
|
||||
$url = Url::fromString($this->application->fqdn);
|
||||
$host = $url->getHost();
|
||||
$schema = $url->getScheme();
|
||||
$random = new Cuid2(7);
|
||||
@@ -137,21 +152,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
// ray()->measure();
|
||||
$containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id);
|
||||
if ($containers->count() === 1) {
|
||||
$this->currently_running_container_name = data_get($containers[0], 'Names');
|
||||
} else {
|
||||
$foundContainer = $containers->filter(function ($container) {
|
||||
return !str(data_get($container, 'Names'))->startsWith("{$this->application->uuid}-pr-");
|
||||
})->first();
|
||||
if ($foundContainer) {
|
||||
$this->currently_running_container_name = data_get($foundContainer, 'Names');
|
||||
}
|
||||
}
|
||||
if ($this->pull_request_id !== 0 && $this->pull_request_id !== null) {
|
||||
$this->currently_running_container_name = $this->container_name;
|
||||
}
|
||||
$this->application_deployment_queue->update([
|
||||
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
|
||||
]);
|
||||
@@ -178,9 +178,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
return "--add-host $name:$ip";
|
||||
})->implode(' ');
|
||||
|
||||
// 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->application->dockerfile_target_build) {
|
||||
$this->buildTarget = " --target {$this->application->dockerfile_target_build} ";
|
||||
}
|
||||
|
||||
// Check custom port
|
||||
preg_match('/(?<=:)\d+(?=\/)/', $this->application->git_repository, $matches);
|
||||
@@ -188,17 +188,27 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
$this->customPort = $matches[0];
|
||||
$gitHost = str($this->application->git_repository)->before(':');
|
||||
$gitRepo = str($this->application->git_repository)->after('/');
|
||||
$this->application->git_repository = "$gitHost:$gitRepo";
|
||||
$this->customRepository = "$gitHost:$gitRepo";
|
||||
} else {
|
||||
$this->customRepository = $this->application->git_repository;
|
||||
}
|
||||
try {
|
||||
if ($this->restart_only) {
|
||||
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 === 'dockerimage') {
|
||||
$this->deploy_dockerimage_buildpack();
|
||||
} else if ($this->application->build_pack === 'dockerfile') {
|
||||
$this->deploy_dockerfile_buildpack();
|
||||
} else if ($this->application->build_pack === 'static') {
|
||||
$this->deploy_static_buildpack();
|
||||
} else {
|
||||
if ($this->pull_request_id !== 0) {
|
||||
$this->deploy_pull_request();
|
||||
@@ -209,21 +219,27 @@ 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();
|
||||
}
|
||||
$this->next(ApplicationDeploymentStatus::FINISHED->value);
|
||||
$this->application->isConfigurationChanged(true);
|
||||
} catch (Exception $e) {
|
||||
ray($e);
|
||||
$this->fail($e);
|
||||
throw $e;
|
||||
} finally {
|
||||
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 > $this->configuration_dir/docker-compose.yml",
|
||||
"echo '{$this->docker_compose_base64}' | base64 -d > $composeFileName",
|
||||
],
|
||||
[
|
||||
"echo '{$readme}' > $this->configuration_dir/README.md",
|
||||
@@ -234,11 +250,53 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
[
|
||||
"docker rm -f {$this->deployment_uuid} >/dev/null 2>&1",
|
||||
"hidden" => true,
|
||||
"ignore_errors" => true,
|
||||
]
|
||||
);
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
"docker image prune -f >/dev/null 2>&1",
|
||||
"hidden" => true,
|
||||
"ignore_errors" => 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 deploy_docker_compose()
|
||||
// {
|
||||
// $dockercompose_base64 = base64_encode($this->application->dockercompose);
|
||||
@@ -253,7 +311,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
// executeInDocker($this->deployment_uuid, "echo '$dockercompose_base64' | base64 -d > $this->workdir/docker-compose.yaml")
|
||||
// ],
|
||||
// );
|
||||
// $this->build_image_name = Str::lower("{$this->application->git_repository}:build");
|
||||
// $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);
|
||||
@@ -278,58 +336,80 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
private function generate_image_names()
|
||||
{
|
||||
if ($this->application->dockerfile) {
|
||||
$this->build_image_name = Str::lower("{$this->application->git_repository}: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->git_repository}:{$tag}-build");
|
||||
$this->production_image_name = Str::lower("{$this->application->uuid}:{$tag}");
|
||||
}
|
||||
}
|
||||
private function just_restart()
|
||||
{
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
"echo 'Starting deployment of {$this->application->git_repository}:{$this->application->git_branch}.'"
|
||||
"echo 'Starting deployment of {$this->customRepository}:{$this->application->git_branch}.'"
|
||||
],
|
||||
);
|
||||
$this->prepare_builder_image();
|
||||
$this->check_git_if_build_needed();
|
||||
$this->set_base_dir();
|
||||
$this->generate_image_names();
|
||||
$this->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->generate_compose_file();
|
||||
$this->rolling_update();
|
||||
return;
|
||||
}
|
||||
$this->execute_remote_command([
|
||||
"echo 'Cannot find image {$this->production_image_name} locally. Please redeploy the application.'",
|
||||
]);
|
||||
throw new RuntimeException('Cannot find image anywhere. Please redeploy the application.');
|
||||
}
|
||||
private function save_environment_variables()
|
||||
private function check_image_locally_or_remotely()
|
||||
{
|
||||
$envs = collect([]);
|
||||
foreach ($this->application->environment_variables as $env) {
|
||||
$envs->push($env->key . '=' . $env->value);
|
||||
$this->execute_remote_command([
|
||||
"docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found"
|
||||
]);
|
||||
if (str($this->saved_outputs->get('local_image_found'))->isEmpty() && $this->application->docker_registry_image_name) {
|
||||
$this->execute_remote_command([
|
||||
"docker pull {$this->production_image_name} 2>/dev/null", "ignore_errors" => true, "hidden" => true
|
||||
]);
|
||||
$this->execute_remote_command([
|
||||
"docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found"
|
||||
]);
|
||||
}
|
||||
$envs_base64 = base64_encode($envs->implode("\n"));
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d > $this->workdir/.env")
|
||||
],
|
||||
);
|
||||
}
|
||||
// private function save_environment_variables()
|
||||
// {
|
||||
// $envs = collect([]);
|
||||
// foreach ($this->application->environment_variables as $env) {
|
||||
// $envs->push($env->key . '=' . $env->value);
|
||||
// }
|
||||
// $envs_base64 = base64_encode($envs->implode("\n"));
|
||||
// $this->execute_remote_command(
|
||||
// [
|
||||
// executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d > $this->workdir/.env")
|
||||
// ],
|
||||
// );
|
||||
// }
|
||||
private function deploy_simple_dockerfile()
|
||||
{
|
||||
$dockerfile_base64 = base64_encode($this->application->dockerfile);
|
||||
@@ -341,7 +421,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
$this->prepare_builder_image();
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "echo '$dockerfile_base64' | base64 -d > $this->workdir/Dockerfile")
|
||||
executeInDocker($this->deployment_uuid, "echo '$dockerfile_base64' | base64 -d > $this->workdir$this->dockerfile_location")
|
||||
],
|
||||
);
|
||||
$this->generate_image_names();
|
||||
@@ -375,7 +455,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
}
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
"echo 'Starting deployment of {$this->application->git_repository}:{$this->application->git_branch}.'"
|
||||
"echo 'Starting deployment of {$this->customRepository}:{$this->application->git_branch}.'"
|
||||
],
|
||||
);
|
||||
$this->prepare_builder_image();
|
||||
@@ -388,13 +468,18 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
$this->generate_build_env_variables();
|
||||
$this->add_build_env_variables_to_dockerfile();
|
||||
$this->build_image();
|
||||
$this->rolling_update();
|
||||
// if ($this->application->additional_destinations) {
|
||||
// $this->push_to_docker_registry();
|
||||
// $this->deploy_to_additional_destinations();
|
||||
// } else {
|
||||
$this->rolling_update();
|
||||
// }
|
||||
}
|
||||
private function deploy_nixpacks_buildpack()
|
||||
{
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
"echo 'Starting deployment of {$this->application->git_repository}:{$this->application->git_branch}.'"
|
||||
"echo 'Starting deployment of {$this->customRepository}:{$this->application->git_branch}.'"
|
||||
],
|
||||
);
|
||||
$this->prepare_builder_image();
|
||||
@@ -402,12 +487,10 @@ 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->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();
|
||||
@@ -428,17 +511,40 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
$this->build_image();
|
||||
$this->rolling_update();
|
||||
}
|
||||
private function deploy_static_buildpack()
|
||||
{
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
"echo 'Starting deployment of {$this->customRepository}:{$this->application->git_branch}.'"
|
||||
],
|
||||
);
|
||||
$this->prepare_builder_image();
|
||||
$this->check_git_if_build_needed();
|
||||
$this->set_base_dir();
|
||||
$this->generate_image_names();
|
||||
$this->clone_repository();
|
||||
$this->cleanup_git();
|
||||
$this->build_image();
|
||||
$this->generate_compose_file();
|
||||
$this->rolling_update();
|
||||
}
|
||||
|
||||
private function rolling_update()
|
||||
{
|
||||
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();
|
||||
@@ -454,17 +560,21 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
}
|
||||
// ray('New container name: ', $this->container_name);
|
||||
if ($this->container_name) {
|
||||
$counter = 0;
|
||||
$counter = 1;
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
"echo 'Waiting for healthcheck to pass on the new version of your application.'"
|
||||
],
|
||||
"echo 'Waiting for healthcheck to pass on the new container.'"
|
||||
]
|
||||
);
|
||||
while ($counter < $this->application->health_check_retries) {
|
||||
if ($this->full_healthcheck_url) {
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
"echo 'Attempt {$counter} of {$this->application->health_check_retries}'"
|
||||
],
|
||||
"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,
|
||||
@@ -474,17 +584,17 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
);
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
"echo 'New version healthcheck status: {$this->saved_outputs->get('health_check')}'"
|
||||
"echo 'Attempt {$counter} of {$this->application->health_check_retries} | Healthcheck status: {$this->saved_outputs->get('health_check')}'"
|
||||
],
|
||||
);
|
||||
if (Str::of($this->saved_outputs->get('health_check'))->contains('healthy')) {
|
||||
$this->newVersionIsHealthy = true;
|
||||
$this->application->update(['status' => 'running']);
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
"echo 'Rolling update completed.'"
|
||||
"echo 'New container is healthy.'"
|
||||
],
|
||||
);
|
||||
$this->application->update(['status' => 'running']);
|
||||
break;
|
||||
}
|
||||
$counter++;
|
||||
@@ -494,9 +604,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
}
|
||||
private function deploy_pull_request()
|
||||
{
|
||||
$this->newVersionIsHealthy = true;
|
||||
$this->generate_image_names();
|
||||
$this->execute_remote_command([
|
||||
"echo 'Starting pull request (#{$this->pull_request_id}) deployment of {$this->application->git_repository}:{$this->application->git_branch}.'",
|
||||
"echo 'Starting pull request (#{$this->pull_request_id}) deployment of {$this->customRepository}:{$this->application->git_branch}.'",
|
||||
]);
|
||||
$this->prepare_builder_image();
|
||||
$this->clone_repository();
|
||||
@@ -510,12 +621,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
// $this->generate_build_env_variables();
|
||||
// $this->add_build_env_variables_to_dockerfile();
|
||||
$this->build_image();
|
||||
if ($this->currently_running_container_name) {
|
||||
$this->execute_remote_command(
|
||||
["echo -n 'Removing old version of your application.'"],
|
||||
[executeInDocker($this->deployment_uuid, "docker rm -f $this->currently_running_container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true],
|
||||
);
|
||||
}
|
||||
$this->stop_running_container();
|
||||
$this->execute_remote_command(
|
||||
["echo -n 'Starting preview deployment.'"],
|
||||
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d"), "hidden" => true],
|
||||
@@ -525,12 +631,15 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
private function prepare_builder_image()
|
||||
{
|
||||
$helperImage = config('coolify.helper_image');
|
||||
// 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} -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}";
|
||||
}
|
||||
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
"echo -n 'Preparing container with helper image: $helperImage.'",
|
||||
@@ -544,7 +653,31 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
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(
|
||||
@@ -594,7 +727,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
$importCommands = $this->generate_git_import_commands();
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
"echo -n 'Importing {$this->application->git_repository}:{$this->application->git_branch} (commit sha {$this->application->git_commit_sha}) to {$this->basedir}. '"
|
||||
"echo '\n----------------------------------------'",
|
||||
],
|
||||
[
|
||||
"echo -n 'Importing {$this->customRepository}:{$this->application->git_branch} (commit sha {$this->application->git_commit_sha}) to {$this->basedir}. '"
|
||||
],
|
||||
[
|
||||
$importCommands, "hidden" => true
|
||||
@@ -619,43 +755,57 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
|
||||
if ($this->source->getMorphClass() == 'App\Models\GithubApp') {
|
||||
if ($this->source->is_public) {
|
||||
$this->fullRepoUrl = "{$this->source->html_url}/{$this->application->git_repository}";
|
||||
$git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$this->application->git_repository} {$this->basedir}";
|
||||
$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->application->git_repository}.git {$this->basedir}"));
|
||||
$this->fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$this->application->git_repository}.git";
|
||||
$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 pull/{$this->pull_request_id}/head:$pr_branch_name && git checkout $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->application->git_repository;
|
||||
$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.');
|
||||
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 = "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->application->git_repository} {$this->basedir}";
|
||||
$git_clone_command = $this->set_git_import_settings($git_clone_command);
|
||||
$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"),
|
||||
executeInDocker($this->deployment_uuid, $git_clone_command)
|
||||
]);
|
||||
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->application->git_repository;
|
||||
$git_clone_command = "{$git_clone_command} {$this->application->git_repository} {$this->basedir}";
|
||||
$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(' && ');
|
||||
@@ -685,16 +835,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")],
|
||||
@@ -754,21 +898,22 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
$labels = collect(generateLabelsApplication($this->application, $this->preview));
|
||||
}
|
||||
if ($this->pull_request_id !== 0) {
|
||||
$newLabels = 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');
|
||||
});
|
||||
$labels = collect(generateLabelsApplication($this->application, $this->preview));
|
||||
|
||||
$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);
|
||||
// $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 = [
|
||||
@@ -798,7 +943,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
'memswap_limit' => $this->application->limits_memory_swap,
|
||||
'mem_swappiness' => $this->application->limits_memory_swappiness,
|
||||
'mem_reservation' => $this->application->limits_memory_reservation,
|
||||
'cpus' => $this->application->limits_cpus,
|
||||
'cpus' => (int) $this->application->limits_cpus,
|
||||
'cpuset' => $this->application->limits_cpuset,
|
||||
'cpu_shares' => $this->application->limits_cpu_shares,
|
||||
]
|
||||
@@ -811,6 +956,36 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
]
|
||||
]
|
||||
];
|
||||
if ($this->server->isLogDrainEnabled() && $this->application->isLogDrainEnabled()) {
|
||||
$docker_compose['services'][$this->container_name]['logging'] = [
|
||||
'driver' => 'fluentd',
|
||||
'options' => [
|
||||
'fluentd-address' => "tcp://127.0.0.1:24224",
|
||||
'fluentd-async' => "true",
|
||||
'fluentd-sub-second-precision' => "true",
|
||||
]
|
||||
];
|
||||
}
|
||||
if ($this->application->settings->is_gpu_enabled) {
|
||||
ray('asd');
|
||||
$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');
|
||||
}
|
||||
@@ -877,11 +1052,17 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
foreach ($this->application->runtime_environment_variables as $env) {
|
||||
$environment_variables->push("$env->key=$env->value");
|
||||
}
|
||||
foreach ($this->application->nixpacks_environment_variables as $env) {
|
||||
$environment_variables->push("$env->key=$env->value");
|
||||
}
|
||||
} else {
|
||||
// ray($this->application->runtime_environment_variables_preview)->green();
|
||||
foreach ($this->application->runtime_environment_variables_preview as $env) {
|
||||
$environment_variables->push("$env->key=$env->value");
|
||||
}
|
||||
foreach ($this->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::of($env)->contains('PORT'))->isEmpty()) {
|
||||
@@ -902,35 +1083,56 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
$health_check_port = $this->application->health_check_port;
|
||||
}
|
||||
if ($this->application->health_check_path) {
|
||||
$this->full_healthcheck_url = "{$this->application->health_check_method}: {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}{$this->application->health_check_path}";
|
||||
$generated_healthchecks_commands = [
|
||||
"curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}{$this->application->health_check_path} > /dev/null"
|
||||
];
|
||||
} else {
|
||||
$this->full_healthcheck_url = "{$this->application->health_check_method}: {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}/";
|
||||
$generated_healthchecks_commands = [
|
||||
"curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}/"
|
||||
];
|
||||
}
|
||||
return implode(' ', $generated_healthchecks_commands);
|
||||
}
|
||||
private function pull_latest_image($image)
|
||||
{
|
||||
$this->execute_remote_command(
|
||||
["echo -n 'Pulling latest image ($image) from the registry.'"],
|
||||
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "docker pull {$image}"), "hidden" => true
|
||||
]
|
||||
);
|
||||
}
|
||||
private function build_image()
|
||||
{
|
||||
$this->execute_remote_command([
|
||||
"echo -n 'Building docker image for your application. To check the current progress, click on Show Debug Logs.'",
|
||||
]);
|
||||
|
||||
if ($this->application->settings->is_static) {
|
||||
if ($this->application->build_pack === 'static') {
|
||||
$this->execute_remote_command([
|
||||
executeInDocker($this->deployment_uuid, "docker build $this->addHosts --network host -f {$this->workdir}/{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"), "hidden" => true
|
||||
"echo -n 'Static deployment. Copying static assets to the image.'",
|
||||
]);
|
||||
} else {
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
"echo -n 'Building docker image started.'",
|
||||
],
|
||||
["echo -n 'To check the current progress, click on Show Debug Logs.'"]
|
||||
);
|
||||
}
|
||||
|
||||
$dockerfile = base64_encode("FROM {$this->application->static_image}
|
||||
if ($this->application->settings->is_static || $this->application->build_pack === 'static') {
|
||||
if ($this->application->static_image) {
|
||||
$this->pull_latest_image($this->application->static_image);
|
||||
}
|
||||
if ($this->application->build_pack === 'static') {
|
||||
$dockerfile = base64_encode("FROM {$this->application->static_image}
|
||||
WORKDIR /usr/share/nginx/html/
|
||||
LABEL coolify.deploymentId={$this->deployment_uuid}
|
||||
COPY --from=$this->build_image_name /app/{$this->application->publish_directory} .
|
||||
COPY . .
|
||||
RUN rm -f /usr/share/nginx/html/nginx.conf
|
||||
RUN rm -f /usr/share/nginx/html/Dockerfile
|
||||
COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
|
||||
$nginx_config = base64_encode("server {
|
||||
$nginx_config = base64_encode("server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name localhost;
|
||||
@@ -946,47 +1148,108 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}");
|
||||
} 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->build_image_name {$this->workdir}"), "hidden" => true
|
||||
]);
|
||||
|
||||
$dockerfile = base64_encode("FROM {$this->application->static_image}
|
||||
WORKDIR /usr/share/nginx/html/
|
||||
LABEL coolify.deploymentId={$this->deployment_uuid}
|
||||
COPY --from=$this->build_image_name /app/{$this->application->publish_directory} .
|
||||
COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
|
||||
$nginx_config = base64_encode("server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
try_files \$uri \$uri.html \$uri/index.html \$uri/ /index.html =404;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}");
|
||||
}
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "echo '{$dockerfile}' | base64 -d > {$this->workdir}/Dockerfile-prod")
|
||||
executeInDocker($this->deployment_uuid, "echo '{$dockerfile}' | base64 -d > {$this->workdir}/Dockerfile")
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "echo '{$nginx_config}' | base64 -d > {$this->workdir}/nginx.conf")
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "docker build $this->addHosts --network host -f {$this->workdir}/Dockerfile-prod {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
|
||||
executeInDocker($this->deployment_uuid, "docker build $this->addHosts --network host -f {$this->workdir}/Dockerfile {$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->addHosts --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
|
||||
]);
|
||||
// Pure Dockerfile based deployment
|
||||
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)
|
||||
{
|
||||
if ($this->currently_running_container_name) {
|
||||
if ($this->newVersionIsHealthy || $force) {
|
||||
$this->execute_remote_command(
|
||||
["echo -n 'Removing old version of your application.'"],
|
||||
[executeInDocker($this->deployment_uuid, "docker rm -f $this->currently_running_container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true],
|
||||
);
|
||||
$this->execute_remote_command(["echo -n 'Removing old container.'"]);
|
||||
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 {
|
||||
$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],
|
||||
);
|
||||
$containers = $containers->filter(function ($container) {
|
||||
return data_get($container, 'Names') !== $this->container_name;
|
||||
});
|
||||
}
|
||||
$containers->each(function ($container) {
|
||||
$containerName = data_get($container, 'Names');
|
||||
$this->execute_remote_command(
|
||||
[executeInDocker($this->deployment_uuid, "docker rm -f $containerName >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true],
|
||||
);
|
||||
});
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
"echo 'Rolling update completed.'"
|
||||
],
|
||||
);
|
||||
} else {
|
||||
$this->execute_remote_command(
|
||||
["echo -n 'New container is not healthy, rolling back to the old container.'"],
|
||||
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function start_by_compose_file()
|
||||
{
|
||||
$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],
|
||||
);
|
||||
if ($this->application->build_pack === 'dockerimage') {
|
||||
$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],
|
||||
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true],
|
||||
);
|
||||
} else {
|
||||
$this->execute_remote_command(
|
||||
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function generate_build_env_variables()
|
||||
@@ -1008,7 +1271,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
private function add_build_env_variables_to_dockerfile()
|
||||
{
|
||||
$this->execute_remote_command([
|
||||
executeInDocker($this->deployment_uuid, "cat {$this->workdir}/{$this->dockerfile_location}"), "hidden" => true, "save" => 'dockerfile'
|
||||
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"));
|
||||
|
||||
@@ -1017,7 +1280,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
}
|
||||
$dockerfile_base64 = base64_encode($dockerfile->implode("\n"));
|
||||
$this->execute_remote_command([
|
||||
executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d > {$this->workdir}/{$this->dockerfile_location}"),
|
||||
executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d > {$this->workdir}{$this->dockerfile_location}"),
|
||||
"hidden" => true
|
||||
]);
|
||||
}
|
||||
@@ -1042,9 +1305,12 @@ 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 'Oops something is not okay, are you okay? 😢'", 'type' => 'err'],
|
||||
["echo '{$exception->getMessage()}'", 'type' => 'err'],
|
||||
["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);
|
||||
}
|
||||
}
|
||||
|
||||
28
app/Jobs/ApplicationRestartJob.php
Normal file
28
app/Jobs/ApplicationRestartJob.php
Normal 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');
|
||||
}
|
||||
}
|
||||
88
app/Jobs/CheckLogDrainContainerJob.php
Normal file
88
app/Jobs/CheckLogDrainContainerJob.php
Normal 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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
40
app/Jobs/CleanupHelperContainersJob.php
Normal file
40
app/Jobs/CleanupHelperContainersJob.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Server;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class CleanupHelperContainersJob implements ShouldQueue, ShouldBeUnique, ShouldBeEncrypted
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public function __construct(public Server $server)
|
||||
{
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
ray('Cleaning up helper containers on ' . $this->server->name);
|
||||
$containers = instant_remote_process(['docker container ps --filter "ancestor=ghcr.io/coollabsio/coolify-helper:next" --filter "ancestor=ghcr.io/coollabsio/coolify-helper:latest" --format \'{{json .}}\''], $this->server, false);
|
||||
$containers = format_docker_command_output_to_json($containers);
|
||||
if ($containers->count() > 0) {
|
||||
foreach ($containers as $container) {
|
||||
$containerId = data_get($container,'ID');
|
||||
ray('Removing container ' . $containerId);
|
||||
instant_remote_process(['docker container rm -f ' . $containerId], $this->server, false);
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
send_internal_notification('CleanupHelperContainersJob failed with error: ' . $e->getMessage());
|
||||
ray($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,6 @@ use App\Models\ApplicationPreview;
|
||||
use App\Models\Server;
|
||||
use App\Notifications\Container\ContainerRestarted;
|
||||
use App\Notifications\Container\ContainerStopped;
|
||||
use App\Notifications\Server\Revived;
|
||||
use App\Notifications\Server\Unreachable;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
@@ -39,78 +37,11 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
// ray("checking server status for {$this->server->id}");
|
||||
// ray("checking container statuses for {$this->server->id}");
|
||||
try {
|
||||
// ray()->clearAll();
|
||||
$serverUptimeCheckNumber = $this->server->unreachable_count;
|
||||
$serverUptimeCheckNumberMax = 3;
|
||||
|
||||
// ray('checking # ' . $serverUptimeCheckNumber);
|
||||
if ($serverUptimeCheckNumber >= $serverUptimeCheckNumberMax) {
|
||||
if ($this->server->unreachable_email_sent === false) {
|
||||
ray('Server unreachable, sending notification...');
|
||||
$this->server->team->notify(new Unreachable($this->server));
|
||||
$this->server->update(['unreachable_email_sent' => true]);
|
||||
}
|
||||
$this->server->settings()->update([
|
||||
'is_reachable' => false,
|
||||
]);
|
||||
$this->server->update([
|
||||
'unreachable_count' => 0,
|
||||
]);
|
||||
// Update all applications, databases and services to exited
|
||||
foreach ($this->server->applications() as $application) {
|
||||
$application->update(['status' => 'exited']);
|
||||
}
|
||||
foreach ($this->server->databases() as $database) {
|
||||
$database->update(['status' => 'exited']);
|
||||
}
|
||||
foreach ($this->server->services() as $service) {
|
||||
$apps = $service->applications()->get();
|
||||
$dbs = $service->databases()->get();
|
||||
foreach ($apps as $app) {
|
||||
$app->update(['status' => 'exited']);
|
||||
}
|
||||
foreach ($dbs as $db) {
|
||||
$db->update(['status' => 'exited']);
|
||||
}
|
||||
}
|
||||
if (!$this->server->isServerReady()) {
|
||||
return;
|
||||
}
|
||||
$result = $this->server->validateConnection();
|
||||
if ($result) {
|
||||
$this->server->settings()->update([
|
||||
'is_reachable' => true,
|
||||
]);
|
||||
$this->server->update([
|
||||
'unreachable_count' => 0,
|
||||
]);
|
||||
} else {
|
||||
$serverUptimeCheckNumber++;
|
||||
$this->server->settings()->update([
|
||||
'is_reachable' => false,
|
||||
]);
|
||||
$this->server->update([
|
||||
'unreachable_count' => $serverUptimeCheckNumber,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data_get($this->server, 'unreachable_email_sent') === true) {
|
||||
ray('Server is reachable again, sending notification...');
|
||||
$this->server->team->notify(new Revived($this->server));
|
||||
$this->server->update(['unreachable_email_sent' => false]);
|
||||
}
|
||||
if (
|
||||
data_get($this->server, 'settings.is_reachable') === false ||
|
||||
data_get($this->server, 'settings.is_usable') === false
|
||||
) {
|
||||
$this->server->settings()->update([
|
||||
'is_reachable' => true,
|
||||
'is_usable' => true
|
||||
]);
|
||||
}
|
||||
// $this->server->validateDockerEngine(true);
|
||||
};
|
||||
$containers = instant_remote_process(["docker container ls -q"], $this->server);
|
||||
if (!$containers) {
|
||||
return;
|
||||
@@ -121,29 +52,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
|
||||
$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 = [];
|
||||
@@ -159,6 +68,9 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
|
||||
if ($applicationId) {
|
||||
$pullRequestId = data_get($labels, 'coolify.pullRequestId');
|
||||
if ($pullRequestId) {
|
||||
if (str($applicationId)->contains('-')) {
|
||||
$applicationId = str($applicationId)->before('-');
|
||||
}
|
||||
$preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first();
|
||||
if ($preview) {
|
||||
$foundApplicationPreviews[] = $preview->id;
|
||||
@@ -252,6 +164,8 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
|
||||
|
||||
if ($projectUuid && $serviceUuid && $environmentName) {
|
||||
$url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/service/" . $serviceUuid;
|
||||
} else {
|
||||
$url = null;
|
||||
}
|
||||
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
|
||||
$exitedService->update(['status' => 'exited']);
|
||||
@@ -276,6 +190,8 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
|
||||
|
||||
if ($projectUuid && $applicationUuid && $environment) {
|
||||
$url = base_url() . '/project/' . $projectUuid . "/" . $environment . "/application/" . $applicationUuid;
|
||||
} else {
|
||||
$url = null;
|
||||
}
|
||||
|
||||
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
|
||||
@@ -299,6 +215,8 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
|
||||
|
||||
if ($projectUuid && $applicationUuid && $environmentName) {
|
||||
$url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/application/" . $applicationUuid;
|
||||
} else {
|
||||
$url = null;
|
||||
}
|
||||
|
||||
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
|
||||
@@ -322,11 +240,37 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
|
||||
|
||||
if ($projectUuid && $databaseUuid && $environmentName) {
|
||||
$url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/database/" . $databaseUuid;
|
||||
} else {
|
||||
$url = null;
|
||||
}
|
||||
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
|
||||
}
|
||||
|
||||
// Check if proxy is running
|
||||
$this->server->proxyType();
|
||||
$foundProxyContainer = $containers->filter(function ($value, $key) {
|
||||
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);
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use App\Models\S3Storage;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Models\ScheduledDatabaseBackupExecution;
|
||||
use App\Models\Server;
|
||||
use App\Models\ServiceDatabase;
|
||||
use App\Models\StandaloneMariadb;
|
||||
use App\Models\StandaloneMongodb;
|
||||
use App\Models\StandaloneMysql;
|
||||
@@ -32,9 +33,10 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
|
||||
public ?Team $team = null;
|
||||
public Server $server;
|
||||
public ScheduledDatabaseBackup $backup;
|
||||
public StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $database;
|
||||
public StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|ServiceDatabase $database;
|
||||
|
||||
public ?string $container_name = null;
|
||||
public ?string $directory_name = null;
|
||||
public ?ScheduledDatabaseBackupExecution $backup_log = null;
|
||||
public string $backup_status = 'failed';
|
||||
public ?string $backup_location = null;
|
||||
@@ -48,9 +50,15 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
|
||||
{
|
||||
$this->backup = $backup;
|
||||
$this->team = Team::find($backup->team_id);
|
||||
$this->database = data_get($this->backup, 'database');
|
||||
$this->server = $this->database->destination->server;
|
||||
$this->s3 = $this->backup->s3;
|
||||
if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') {
|
||||
$this->database = data_get($this->backup, 'database');
|
||||
$this->server = $this->database->service->server;
|
||||
$this->s3 = $this->backup->s3;
|
||||
} else {
|
||||
$this->database = data_get($this->backup, 'database');
|
||||
$this->server = $this->database->destination->server;
|
||||
$this->s3 = $this->backup->s3;
|
||||
}
|
||||
}
|
||||
|
||||
public function middleware(): array
|
||||
@@ -73,14 +81,108 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
|
||||
$this->database->delete();
|
||||
return;
|
||||
}
|
||||
|
||||
$status = Str::of(data_get($this->database, 'status'));
|
||||
if (!$status->startsWith('running') && $this->database->id !== 0) {
|
||||
ray('database not running');
|
||||
return;
|
||||
}
|
||||
$databaseType = $this->database->type();
|
||||
$databasesToBackup = data_get($this->backup, 'databases_to_backup');
|
||||
if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') {
|
||||
$databaseType = $this->database->databaseType();
|
||||
$serviceUuid = $this->database->service->uuid;
|
||||
$serviceName = str($this->database->service->name)->slug();
|
||||
if ($databaseType === 'standalone-postgresql') {
|
||||
$this->container_name = "{$this->database->name}-$serviceUuid";
|
||||
$this->directory_name = $serviceName . '-' . $this->container_name;
|
||||
$commands[] = "docker exec $this->container_name env | grep POSTGRES_";
|
||||
$envs = instant_remote_process($commands, $this->server);
|
||||
$envs = str($envs)->explode("\n");
|
||||
|
||||
$user = $envs->filter(function ($env) {
|
||||
return str($env)->startsWith('POSTGRES_USER=');
|
||||
})->first();
|
||||
if ($user) {
|
||||
$this->database->postgres_user = str($user)->after('POSTGRES_USER=')->value();
|
||||
} else {
|
||||
$this->database->postgres_user = 'postgres';
|
||||
}
|
||||
|
||||
$db = $envs->filter(function ($env) {
|
||||
return str($env)->startsWith('POSTGRES_DB=');
|
||||
})->first();
|
||||
|
||||
if ($db) {
|
||||
$databasesToBackup = str($db)->after('POSTGRES_DB=')->value();
|
||||
} else {
|
||||
$databasesToBackup = $this->database->postgres_user;
|
||||
}
|
||||
} else if ($databaseType === 'standalone-mysql') {
|
||||
$this->container_name = "{$this->database->name}-$serviceUuid";
|
||||
$this->directory_name = $serviceName . '-' . $this->container_name;
|
||||
$commands[] = "docker exec $this->container_name env | grep MYSQL_";
|
||||
$envs = instant_remote_process($commands, $this->server);
|
||||
$envs = str($envs)->explode("\n");
|
||||
|
||||
$rootPassword = $envs->filter(function ($env) {
|
||||
return str($env)->startsWith('MYSQL_ROOT_PASSWORD=');
|
||||
})->first();
|
||||
if ($rootPassword) {
|
||||
$this->database->mysql_root_password = str($rootPassword)->after('MYSQL_ROOT_PASSWORD=')->value();
|
||||
}
|
||||
|
||||
$db = $envs->filter(function ($env) {
|
||||
return str($env)->startsWith('MYSQL_DATABASE=');
|
||||
})->first();
|
||||
|
||||
if ($db) {
|
||||
$databasesToBackup = str($db)->after('MYSQL_DATABASE=')->value();
|
||||
} else {
|
||||
throw new \Exception('MYSQL_DATABASE not found');
|
||||
}
|
||||
} else if ($databaseType === 'standalone-mariadb') {
|
||||
$this->container_name = "{$this->database->name}-$serviceUuid";
|
||||
$this->directory_name = $serviceName . '-' . $this->container_name;
|
||||
$commands[] = "docker exec $this->container_name env";
|
||||
$envs = instant_remote_process($commands, $this->server);
|
||||
$envs = str($envs)->explode("\n");
|
||||
$rootPassword = $envs->filter(function ($env) {
|
||||
return str($env)->startsWith('MARIADB_ROOT_PASSWORD=');
|
||||
})->first();
|
||||
if ($rootPassword) {
|
||||
$this->database->mariadb_root_password = str($rootPassword)->after('MARIADB_ROOT_PASSWORD=')->value();
|
||||
} else {
|
||||
$rootPassword = $envs->filter(function ($env) {
|
||||
return str($env)->startsWith('MYSQL_ROOT_PASSWORD=');
|
||||
})->first();
|
||||
if ($rootPassword) {
|
||||
$this->database->mariadb_root_password = str($rootPassword)->after('MYSQL_ROOT_PASSWORD=')->value();
|
||||
}
|
||||
}
|
||||
|
||||
$db = $envs->filter(function ($env) {
|
||||
return str($env)->startsWith('MARIADB_DATABASE=');
|
||||
})->first();
|
||||
|
||||
if ($db) {
|
||||
$databasesToBackup = str($db)->after('MARIADB_DATABASE=')->value();
|
||||
} else {
|
||||
$db = $envs->filter(function ($env) {
|
||||
return str($env)->startsWith('MYSQL_DATABASE=');
|
||||
})->first();
|
||||
|
||||
if ($db) {
|
||||
$databasesToBackup = str($db)->after('MYSQL_DATABASE=')->value();
|
||||
} else {
|
||||
throw new \Exception('MARIADB_DATABASE or MYSQL_DATABASE not found');
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$databaseName = str($this->database->name)->slug()->value();
|
||||
$this->container_name = $this->database->uuid;
|
||||
$this->directory_name = $databaseName . '-' . $this->container_name;
|
||||
$databaseType = $this->database->type();
|
||||
$databasesToBackup = data_get($this->backup, 'databases_to_backup');
|
||||
}
|
||||
|
||||
if (is_null($databasesToBackup)) {
|
||||
if ($databaseType === 'standalone-postgresql') {
|
||||
@@ -116,12 +218,11 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
|
||||
return;
|
||||
}
|
||||
}
|
||||
$this->container_name = $this->database->uuid;
|
||||
$this->backup_dir = backup_dir() . "/databases/" . Str::of($this->team->name)->slug() . '-' . $this->team->id . '/' . $this->container_name;
|
||||
$this->backup_dir = backup_dir() . "/databases/" . Str::of($this->team->name)->slug() . '-' . $this->team->id . '/' . $this->directory_name;
|
||||
|
||||
if ($this->database->name === 'coolify-db') {
|
||||
$databasesToBackup = ['coolify'];
|
||||
$this->container_name = "coolify-db";
|
||||
$this->directory_name = $this->container_name = "coolify-db";
|
||||
$ip = Str::slug($this->server->ip);
|
||||
$this->backup_dir = backup_dir() . "/coolify" . "/coolify-db-$ip";
|
||||
}
|
||||
@@ -314,7 +415,7 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
|
||||
if ($this->backup->number_of_backups_locally === 0) {
|
||||
$deletable = $this->backup->executions()->where('status', 'success');
|
||||
} else {
|
||||
$deletable = $this->backup->executions()->where('status', 'success')->orderByDesc('created_at')->skip($this->backup->number_of_backups_locally);
|
||||
$deletable = $this->backup->executions()->where('status', 'success')->orderByDesc('created_at')->skip($this->backup->number_of_backups_locally - 1);
|
||||
}
|
||||
foreach ($deletable->get() as $execution) {
|
||||
delete_backup_locally($execution->filename, $this->server);
|
||||
@@ -333,9 +434,13 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
|
||||
// $region = $this->s3->region;
|
||||
$bucket = $this->s3->bucket;
|
||||
$endpoint = $this->s3->endpoint;
|
||||
$this->s3->testConnection();
|
||||
$commands[] = "docker run --pull=always -d --network {$this->database->destination->network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro ghcr.io/coollabsio/coolify-helper >/dev/null 2>&1";
|
||||
|
||||
$this->s3->testConnection(shouldSave: true);
|
||||
if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') {
|
||||
$network = $this->database->service->destination->network;
|
||||
} else {
|
||||
$network = $this->database->destination->network;
|
||||
}
|
||||
$commands[] = "docker run --pull=always -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro ghcr.io/coollabsio/coolify-helper";
|
||||
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc config host add temporary {$endpoint} $key $secret";
|
||||
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc cp $this->backup_location temporary/$bucket{$this->backup_dir}/";
|
||||
instant_remote_process($commands, $this->server);
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
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;
|
||||
@@ -11,68 +12,57 @@ use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $timeout = 1000;
|
||||
public ?string $dockerRootFilesystem = null;
|
||||
public $timeout = 300;
|
||||
public ?int $usageBefore = null;
|
||||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
|
||||
}
|
||||
|
||||
public function uniqueId(): string
|
||||
{
|
||||
return $this->server->uuid;
|
||||
}
|
||||
public function __construct(public Server $server)
|
||||
{
|
||||
}
|
||||
public function handle(): void
|
||||
{
|
||||
$queuedCount = 0;
|
||||
$this->server->applications()->each(function ($application) use ($queuedCount) {
|
||||
$count = data_get($application->deployments(), 'count', 0);
|
||||
$queuedCount += $count;
|
||||
});
|
||||
if ($queuedCount > 0) {
|
||||
ray('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping')->color('orange');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$isInprogress = false;
|
||||
$this->server->applications()->each(function ($application) use (&$isInprogress) {
|
||||
if ($application->isDeploymentInprogress()) {
|
||||
$isInprogress = true;
|
||||
return;
|
||||
}
|
||||
});
|
||||
if ($isInprogress) {
|
||||
throw new Exception('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping...');
|
||||
}
|
||||
if (!$this->server->isFunctional()) {
|
||||
return;
|
||||
}
|
||||
$this->dockerRootFilesystem = "/";
|
||||
$this->usageBefore = $this->getFilesystemUsage();
|
||||
$this->usageBefore = $this->server->getDiskUsage();
|
||||
ray('Usage before: ' . $this->usageBefore);
|
||||
if ($this->usageBefore >= $this->server->settings->cleanup_after_percentage) {
|
||||
ray('Cleaning up ' . $this->server->name)->color('orange');
|
||||
instant_remote_process(['docker image prune -af'], $this->server);
|
||||
instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $this->server);
|
||||
instant_remote_process(['docker builder prune -af'], $this->server);
|
||||
$usageAfter = $this->getFilesystemUsage();
|
||||
ray('Cleaning up ' . $this->server->name);
|
||||
instant_remote_process(['docker image prune -af'], $this->server, false);
|
||||
instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $this->server, false);
|
||||
instant_remote_process(['docker builder prune -af'], $this->server, false);
|
||||
$usageAfter = $this->server->getDiskUsage();
|
||||
if ($usageAfter < $this->usageBefore) {
|
||||
ray('Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name)->color('orange');
|
||||
ray('Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name);
|
||||
send_internal_notification('DockerCleanupJob done: Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name);
|
||||
Log::info('DockerCleanupJob done: Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name);
|
||||
} else {
|
||||
ray('DockerCleanupJob failed to save disk space on ' . $this->server->name)->color('orange');
|
||||
Log::info('DockerCleanupJob failed to save disk space on ' . $this->server->name);
|
||||
}
|
||||
} else {
|
||||
ray('No need to clean up ' . $this->server->name)->color('orange');
|
||||
ray('No need to clean up ' . $this->server->name);
|
||||
Log::info('No need to clean up ' . $this->server->name);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
send_internal_notification('DockerCleanupJob failed with: ' . $e->getMessage());
|
||||
ray($e->getMessage())->color('orange');
|
||||
ray($e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function getFilesystemUsage()
|
||||
{
|
||||
return instant_remote_process(["df '{$this->dockerRootFilesystem}'| tail -1 | awk '{ print $5}' | sed 's/%//g'"], $this->server, false);
|
||||
}
|
||||
}
|
||||
|
||||
1165
app/Jobs/MultipleApplicationDeploymentJob.php
Normal file
1165
app/Jobs/MultipleApplicationDeploymentJob.php
Normal file
File diff suppressed because it is too large
Load Diff
68
app/Jobs/ServerStatusJob.php
Normal file
68
app/Jobs/ServerStatusJob.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Server;
|
||||
use App\Notifications\Server\HighDiskUsage;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ServerStatusJob implements ShouldQueue, ShouldBeEncrypted
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public ?int $disk_usage = null;
|
||||
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 handle(): void
|
||||
{
|
||||
ray("checking server status for {$this->server->id}");
|
||||
try {
|
||||
if ($this->server->isServerReady()) {
|
||||
$this->cleanup(notify: false);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
send_internal_notification('ServerStatusJob failed with: ' . $e->getMessage());
|
||||
ray($e->getMessage());
|
||||
handleError($e);
|
||||
}
|
||||
}
|
||||
public function cleanup(bool $notify = false): void
|
||||
{
|
||||
$this->disk_usage = $this->server->getDiskUsage();
|
||||
if ($this->disk_usage >= $this->server->settings->cleanup_after_percentage) {
|
||||
if ($notify) {
|
||||
if ($this->server->high_disk_usage_notification_sent) {
|
||||
ray('high disk usage notification already sent');
|
||||
return;
|
||||
} else {
|
||||
$this->server->high_disk_usage_notification_sent = true;
|
||||
$this->server->save();
|
||||
$this->server->team->notify(new HighDiskUsage($this->server, $this->disk_usage, $this->server->settings->cleanup_after_percentage));
|
||||
}
|
||||
} else {
|
||||
DockerCleanupJob::dispatchSync($this->server);
|
||||
$this->cleanup(notify: true);
|
||||
}
|
||||
} else {
|
||||
$this->server->high_disk_usage_notification_sent = false;
|
||||
$this->server->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,6 +85,18 @@ class Application extends BaseModel
|
||||
);
|
||||
}
|
||||
|
||||
public function gitWebhook(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) {
|
||||
return "{$this->source->html_url}/{$this->git_repository}/settings/hooks";
|
||||
}
|
||||
return $this->git_repository;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function gitCommits(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
@@ -213,6 +225,15 @@ class Application extends BaseModel
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
public function isDeploymentInprogress()
|
||||
{
|
||||
$deployments = ApplicationDeploymentQueue::where('application_id', $this->id)->where('status', 'in_progress')->count();
|
||||
if ($deployments > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function deployments(int $skip = 0, int $take = 10)
|
||||
{
|
||||
$deployments = ApplicationDeploymentQueue::where('application_id', $this->id)->orderBy('created_at', 'desc');
|
||||
@@ -280,6 +301,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;
|
||||
@@ -307,4 +332,14 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,14 +36,13 @@ class S3Storage extends BaseModel
|
||||
return "{$this->endpoint}/{$this->bucket}";
|
||||
}
|
||||
|
||||
public function testConnection()
|
||||
public function testConnection(bool $shouldSave = false)
|
||||
{
|
||||
try {
|
||||
set_s3_target($this);
|
||||
Storage::disk('custom-s3')->files();
|
||||
$this->unusable_email_sent = false;
|
||||
$this->is_usable = true;
|
||||
return;
|
||||
} catch (\Throwable $e) {
|
||||
$this->is_usable = false;
|
||||
if ($this->unusable_email_sent === false && is_transactional_emails_active()) {
|
||||
@@ -65,7 +64,9 @@ class S3Storage extends BaseModel
|
||||
|
||||
throw $e;
|
||||
} finally {
|
||||
$this->save();
|
||||
if ($shouldSave) {
|
||||
$this->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,15 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Actions\Server\InstallLogDrain;
|
||||
use App\Actions\Server\InstallNewRelic;
|
||||
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\Sleep;
|
||||
use Spatie\SchemalessAttributes\Casts\SchemalessAttributes;
|
||||
use Spatie\SchemalessAttributes\SchemalessAttributesTrait;
|
||||
use Illuminate\Support\Str;
|
||||
@@ -56,6 +61,8 @@ class Server extends BaseModel
|
||||
|
||||
public $casts = [
|
||||
'proxy' => SchemalessAttributes::class,
|
||||
'logdrain_axiom_api_key' => 'encrypted',
|
||||
'logdrain_newrelic_license_key' => 'encrypted',
|
||||
];
|
||||
protected $schemalessAttributes = [
|
||||
'proxy',
|
||||
@@ -109,11 +116,84 @@ class Server extends BaseModel
|
||||
return $this->proxy->modelScope();
|
||||
}
|
||||
|
||||
public function isLocalhost()
|
||||
{
|
||||
return $this->ip === 'host.docker.internal' || $this->id === 0;
|
||||
}
|
||||
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 = 3;
|
||||
|
||||
$currentTime = 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...');
|
||||
$this->team->notify(new Unreachable($this));
|
||||
$this->update(['unreachable_notification_sent' => true]);
|
||||
}
|
||||
$this->settings()->update([
|
||||
'is_reachable' => false,
|
||||
]);
|
||||
$this->update([
|
||||
'unreachable_count' => 0,
|
||||
]);
|
||||
foreach ($this->applications() as $application) {
|
||||
$application->update(['status' => 'exited']);
|
||||
}
|
||||
foreach ($this->databases() as $database) {
|
||||
$database->update(['status' => 'exited']);
|
||||
}
|
||||
foreach ($this->services() as $service) {
|
||||
$apps = $service->applications()->get();
|
||||
$dbs = $service->databases()->get();
|
||||
foreach ($apps as $app) {
|
||||
$app->update(['status' => 'exited']);
|
||||
}
|
||||
foreach ($dbs as $db) {
|
||||
$db->update(['status' => 'exited']);
|
||||
}
|
||||
}
|
||||
$isReady = false;
|
||||
break;
|
||||
}
|
||||
$result = $this->validateConnection();
|
||||
// ray('validateConnection: ' . $result);
|
||||
if (!$result) {
|
||||
$serverUptimeCheckNumber++;
|
||||
$this->update([
|
||||
'unreachable_count' => $serverUptimeCheckNumber,
|
||||
]);
|
||||
Sleep::for(5)->seconds();
|
||||
return;
|
||||
}
|
||||
$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 hasDefinedResources()
|
||||
{
|
||||
$applications = $this->applications()->count() === 0;
|
||||
$databases = $this->databases()->count() === 0;
|
||||
$services = $this->services()->count() === 0;
|
||||
$applications = $this->applications()->count() > 0;
|
||||
$databases = $this->databases()->count() > 0;
|
||||
$services = $this->services()->count() > 0;
|
||||
if ($applications || $databases || $services) {
|
||||
return true;
|
||||
}
|
||||
@@ -148,7 +228,7 @@ class Server extends BaseModel
|
||||
if (isDev()) {
|
||||
return '127.0.0.1';
|
||||
}
|
||||
if ($this->ip === 'host.docker.internal') {
|
||||
if ($this->isLocalhost()) {
|
||||
return base_ip();
|
||||
}
|
||||
return $this->ip;
|
||||
@@ -220,16 +300,57 @@ class Server extends BaseModel
|
||||
{
|
||||
return $this->settings->is_reachable && $this->settings->is_usable;
|
||||
}
|
||||
public function validateConnection()
|
||||
public function isLogDrainEnabled()
|
||||
{
|
||||
$uptime = instant_remote_process(['uptime'], $this, false);
|
||||
if (!$uptime) {
|
||||
$this->settings->is_reachable = false;
|
||||
$this->settings->save();
|
||||
return $this->settings->is_logdrain_newrelic_enabled || $this->settings->is_logdrain_highlight_enabled || $this->settings->is_logdrain_axiom_enabled;
|
||||
}
|
||||
public function validateOS()
|
||||
{
|
||||
$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');
|
||||
// ray($ID, $ID_LIKE, $VERSION_ID);
|
||||
if (collect(SUPPORTED_OS)->contains($ID_LIKE)) {
|
||||
ray('supported');
|
||||
return str($ID_LIKE)->explode(' ')->first();
|
||||
} else {
|
||||
ray('not supported');
|
||||
return false;
|
||||
}
|
||||
$this->settings->is_reachable = true;
|
||||
$this->settings->save();
|
||||
}
|
||||
public function validateConnection()
|
||||
{
|
||||
if ($this->skipServer()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$uptime = instant_remote_process(['uptime'], $this, false);
|
||||
if (!$uptime) {
|
||||
$this->settings()->update([
|
||||
'is_reachable' => false,
|
||||
]);
|
||||
return false;
|
||||
} else {
|
||||
$this->settings()->update([
|
||||
'is_reachable' => true,
|
||||
]);
|
||||
$this->update([
|
||||
'unreachable_count' => 0,
|
||||
]);
|
||||
}
|
||||
|
||||
if (data_get($this, 'unreachable_notification_sent') === true) {
|
||||
$this->team->notify(new Revived($this));
|
||||
$this->update(['unreachable_notification_sent' => false]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
public function validateDockerEngine($throwError = false)
|
||||
@@ -239,7 +360,7 @@ 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;
|
||||
}
|
||||
@@ -257,6 +378,7 @@ class Server extends BaseModel
|
||||
$this->settings->save();
|
||||
return false;
|
||||
}
|
||||
$this->settings->is_reachable = true;
|
||||
$this->settings->is_usable = true;
|
||||
$this->settings->save();
|
||||
return true;
|
||||
|
||||
@@ -45,7 +45,261 @@ class Service extends BaseModel
|
||||
{
|
||||
return 'service';
|
||||
}
|
||||
public function extraFields()
|
||||
{
|
||||
$fields = collect([]);
|
||||
$applications = $this->applications()->get();
|
||||
foreach ($applications as $application) {
|
||||
$image = str($application->image)->before(':')->value();
|
||||
switch ($image) {
|
||||
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();
|
||||
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'):
|
||||
$data = collect([]);
|
||||
$admin_email = $this->environment_variables()->where('key', 'WEBLATE_ADMIN_EMAIL')->first();
|
||||
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_WEBLATE')->first();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
$databases = $this->databases()->get();
|
||||
|
||||
foreach ($databases as $database) {
|
||||
$image = str($database->image)->before(':')->value();
|
||||
switch ($image) {
|
||||
case str($image)->contains('postgres'):
|
||||
$userVariables = ['SERVICE_USER_POSTGRES', 'SERVICE_USER_POSTGRESQL'];
|
||||
$passwordVariables = ['SERVICE_PASSWORD_POSTGRES', 'SERVICE_PASSWORD_POSTGRESQL'];
|
||||
$dbNameVariables = ['POSTGRESQL_DATABASE', 'POSTGRES_DB'];
|
||||
$postgres_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
|
||||
$postgres_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
|
||||
$postgres_db_name = $this->environment_variables()->whereIn('key', $dbNameVariables)->first();
|
||||
$data = collect([]);
|
||||
if ($postgres_user) {
|
||||
$data = $data->merge([
|
||||
'User' => [
|
||||
'key' => data_get($postgres_user, 'key'),
|
||||
'value' => data_get($postgres_user, 'value'),
|
||||
'rules' => 'required',
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($postgres_password) {
|
||||
$data = $data->merge([
|
||||
'Password' => [
|
||||
'key' => data_get($postgres_password, 'key'),
|
||||
'value' => data_get($postgres_password, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($postgres_db_name) {
|
||||
$data = $data->merge([
|
||||
'Database Name' => [
|
||||
'key' => data_get($postgres_db_name, 'key'),
|
||||
'value' => data_get($postgres_db_name, 'value'),
|
||||
'rules' => 'required',
|
||||
],
|
||||
]);
|
||||
}
|
||||
$fields->put('PostgreSQL', $data->toArray());
|
||||
break;
|
||||
case str($image)->contains('mysql'):
|
||||
$userVariables = ['SERVICE_USER_MYSQL', 'SERVICE_USER_WORDPRESS'];
|
||||
$passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS'];
|
||||
$rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT'];
|
||||
$dbNameVariables = ['MYSQL_DATABASE'];
|
||||
$mysql_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
|
||||
$mysql_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
|
||||
$mysql_root_password = $this->environment_variables()->whereIn('key', $rootPasswordVariables)->first();
|
||||
$mysql_db_name = $this->environment_variables()->whereIn('key', $dbNameVariables)->first();
|
||||
$data = collect([]);
|
||||
if ($mysql_user) {
|
||||
$data = $data->merge([
|
||||
'User' => [
|
||||
'key' => data_get($mysql_user, 'key'),
|
||||
'value' => data_get($mysql_user, 'value'),
|
||||
'rules' => 'required',
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($mysql_password) {
|
||||
$data = $data->merge([
|
||||
'Password' => [
|
||||
'key' => data_get($mysql_password, 'key'),
|
||||
'value' => data_get($mysql_password, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($mysql_root_password) {
|
||||
$data = $data->merge([
|
||||
'Root Password' => [
|
||||
'key' => data_get($mysql_root_password, 'key'),
|
||||
'value' => data_get($mysql_root_password, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($mysql_db_name) {
|
||||
$data = $data->merge([
|
||||
'Database Name' => [
|
||||
'key' => data_get($mysql_db_name, 'key'),
|
||||
'value' => data_get($mysql_db_name, 'value'),
|
||||
'rules' => 'required',
|
||||
],
|
||||
]);
|
||||
}
|
||||
$fields->put('MySQL', $data->toArray());
|
||||
break;
|
||||
case str($image)->contains('mariadb'):
|
||||
$userVariables = ['SERVICE_USER_MARIADB', 'SERVICE_USER_WORDPRESS', '_APP_DB_USER'];
|
||||
$passwordVariables = ['SERVICE_PASSWORD_MARIADB', 'SERVICE_PASSWORD_WORDPRESS', '_APP_DB_PASS'];
|
||||
$rootPasswordVariables = ['SERVICE_PASSWORD_MARIADBROOT', 'SERVICE_PASSWORD_ROOT', '_APP_DB_ROOT_PASS'];
|
||||
$dbNameVariables = ['SERVICE_DATABASE_MARIADB', 'SERVICE_DATABASE_WORDPRESS', '_APP_DB_SCHEMA'];
|
||||
$mariadb_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
|
||||
$mariadb_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
|
||||
$mariadb_root_password = $this->environment_variables()->whereIn('key', $rootPasswordVariables)->first();
|
||||
$mariadb_db_name = $this->environment_variables()->whereIn('key', $dbNameVariables)->first();
|
||||
$data = collect([]);
|
||||
|
||||
if ($mariadb_user) {
|
||||
$data = $data->merge([
|
||||
'User' => [
|
||||
'key' => data_get($mariadb_user, 'key'),
|
||||
'value' => data_get($mariadb_user, 'value'),
|
||||
'rules' => 'required',
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($mariadb_password) {
|
||||
$data = $data->merge([
|
||||
'Password' => [
|
||||
'key' => data_get($mariadb_password, 'key'),
|
||||
'value' => data_get($mariadb_password, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($mariadb_root_password) {
|
||||
$data = $data->merge([
|
||||
'Root Password' => [
|
||||
'key' => data_get($mariadb_root_password, 'key'),
|
||||
'value' => data_get($mariadb_root_password, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($mariadb_db_name) {
|
||||
$data = $data->merge([
|
||||
'Database Name' => [
|
||||
'key' => data_get($mariadb_db_name, 'key'),
|
||||
'value' => data_get($mariadb_db_name, 'value'),
|
||||
'rules' => 'required',
|
||||
],
|
||||
]);
|
||||
}
|
||||
$fields->put('MariaDB', $data->toArray());
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $fields;
|
||||
}
|
||||
public function saveExtraFields($fields)
|
||||
{
|
||||
foreach ($fields as $field) {
|
||||
$key = data_get($field, 'key');
|
||||
$value = data_get($field, 'value');
|
||||
$found = $this->environment_variables()->where('key', $key)->first();
|
||||
if ($found) {
|
||||
$found->value = $value;
|
||||
$found->save();
|
||||
} else {
|
||||
$this->environment_variables()->create([
|
||||
'key' => $key,
|
||||
'value' => $value,
|
||||
'is_build_time' => false,
|
||||
'service_id' => $this->id,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
public function documentation()
|
||||
{
|
||||
$services = getServiceTemplates();
|
||||
@@ -146,6 +400,19 @@ class Service extends BaseModel
|
||||
$serviceNetworks = collect(data_get($service, 'networks', []));
|
||||
$serviceVariables = collect(data_get($service, 'environment', []));
|
||||
$serviceLabels = collect(data_get($service, 'labels', []));
|
||||
if ($serviceLabels->count() > 0) {
|
||||
$removedLabels = collect([]);
|
||||
$serviceLabels = $serviceLabels->filter(function ($serviceLabel, $serviceLabelName) use ($removedLabels) {
|
||||
if (!str($serviceLabel)->contains('=')) {
|
||||
$removedLabels->put($serviceLabelName, $serviceLabel);
|
||||
return false;
|
||||
}
|
||||
return $serviceLabel;
|
||||
});
|
||||
foreach($removedLabels as $removedLabelName =>$removedLabel) {
|
||||
$serviceLabels->push("$removedLabelName=$removedLabel");
|
||||
}
|
||||
}
|
||||
|
||||
$containerName = "$serviceName-{$this->uuid}";
|
||||
|
||||
@@ -257,7 +524,7 @@ class Service extends BaseModel
|
||||
}
|
||||
}
|
||||
$networks = collect();
|
||||
foreach ($serviceNetworks as $key =>$serviceNetwork) {
|
||||
foreach ($serviceNetworks as $key => $serviceNetwork) {
|
||||
if (gettype($serviceNetwork) === 'string') {
|
||||
// networks:
|
||||
// - appwrite
|
||||
@@ -268,7 +535,7 @@ class Service extends BaseModel
|
||||
// ipv4_address: 192.168.203.254
|
||||
// $networks->put($serviceNetwork, null);
|
||||
ray($key);
|
||||
$networks->put($key,$serviceNetwork);
|
||||
$networks->put($key, $serviceNetwork);
|
||||
}
|
||||
}
|
||||
foreach ($definedNetwork as $key => $network) {
|
||||
@@ -395,6 +662,7 @@ class Service extends BaseModel
|
||||
$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();
|
||||
@@ -452,15 +720,31 @@ class Service extends BaseModel
|
||||
'service_id' => $this->id,
|
||||
])->first();
|
||||
if ($value->startsWith('SERVICE_')) {
|
||||
$command = $value->after('SERVICE_')->beforeLast('_');
|
||||
$forService = $value->afterLast('_');
|
||||
$generatedValue = null;
|
||||
// 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 {
|
||||
@@ -476,7 +760,7 @@ class Service extends BaseModel
|
||||
]);
|
||||
}
|
||||
if (!$isDatabase) {
|
||||
if ($command->value() === 'FQDN') {
|
||||
if ($command->value() === 'FQDN' && is_null($savedService->fqdn)) {
|
||||
$savedService->fqdn = $fqdn;
|
||||
$savedService->save();
|
||||
}
|
||||
@@ -547,7 +831,11 @@ class Service extends BaseModel
|
||||
}
|
||||
|
||||
// Add labels to the service
|
||||
$fqdns = collect(data_get($savedService, 'fqdns'));
|
||||
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) {
|
||||
@@ -555,6 +843,16 @@ class Service extends BaseModel
|
||||
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($this->uuid, $fqdns, true));
|
||||
}
|
||||
}
|
||||
if ($this->server->isLogDrainEnabled() && $savedService->isLogDrainEnabled()) {
|
||||
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);
|
||||
|
||||
@@ -18,10 +18,24 @@ class ServiceApplication extends BaseModel
|
||||
$service->fileStorages()->delete();
|
||||
});
|
||||
}
|
||||
public function isLogDrainEnabled()
|
||||
{
|
||||
return data_get($this, 'is_log_drain_enabled', false);
|
||||
}
|
||||
public function type()
|
||||
{
|
||||
return 'service';
|
||||
}
|
||||
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 service()
|
||||
{
|
||||
return $this->belongsTo(Service::class);
|
||||
|
||||
@@ -16,10 +16,36 @@ class ServiceDatabase extends BaseModel
|
||||
$service->fileStorages()->delete();
|
||||
});
|
||||
}
|
||||
public function isLogDrainEnabled()
|
||||
{
|
||||
return data_get($this, 'is_log_drain_enabled', false);
|
||||
}
|
||||
public function type()
|
||||
{
|
||||
return 'service';
|
||||
}
|
||||
public function serviceType()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
public function databaseType()
|
||||
{
|
||||
$image = str($this->image)->before(':');
|
||||
if ($image->value() === 'postgres') {
|
||||
$image = 'postgresql';
|
||||
}
|
||||
return "standalone-$image";
|
||||
}
|
||||
public function getServiceDatabaseUrl()
|
||||
{
|
||||
$port = $this->public_port;
|
||||
$realIp = $this->service->server->ip;
|
||||
if ($this->service->server->isLocalhost() || isDev()) {
|
||||
$realIp = base_ip();
|
||||
}
|
||||
$url = "{$realIp}:{$port}";
|
||||
return $url;
|
||||
}
|
||||
public function service()
|
||||
{
|
||||
return $this->belongsTo(Service::class);
|
||||
@@ -36,4 +62,8 @@ class ServiceDatabase extends BaseModel
|
||||
{
|
||||
getFilesystemVolumesFromServer($this, $isInit);
|
||||
}
|
||||
public function scheduledBackups()
|
||||
{
|
||||
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,10 @@ class StandaloneMariadb extends BaseModel
|
||||
$database->environment_variables()->delete();
|
||||
});
|
||||
}
|
||||
public function isLogDrainEnabled()
|
||||
{
|
||||
return data_get($this, 'is_log_drain_enabled', false);
|
||||
}
|
||||
public function type(): string
|
||||
{
|
||||
return 'standalone-mariadb';
|
||||
|
||||
@@ -44,7 +44,10 @@ class StandaloneMongodb extends BaseModel
|
||||
$database->environment_variables()->delete();
|
||||
});
|
||||
}
|
||||
|
||||
public function isLogDrainEnabled()
|
||||
{
|
||||
return data_get($this, 'is_log_drain_enabled', false);
|
||||
}
|
||||
public function mongoInitdbRootPassword(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
|
||||
@@ -46,6 +46,11 @@ class StandaloneMysql extends BaseModel
|
||||
return 'standalone-mysql';
|
||||
}
|
||||
|
||||
public function isLogDrainEnabled()
|
||||
{
|
||||
return data_get($this, 'is_log_drain_enabled', false);
|
||||
}
|
||||
|
||||
public function portsMappings(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
|
||||
@@ -42,6 +42,11 @@ class StandalonePostgresql extends BaseModel
|
||||
});
|
||||
}
|
||||
|
||||
public function isLogDrainEnabled()
|
||||
{
|
||||
return data_get($this, 'is_log_drain_enabled', false);
|
||||
}
|
||||
|
||||
public function portsMappings(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
|
||||
@@ -37,6 +37,11 @@ class StandaloneRedis extends BaseModel
|
||||
});
|
||||
}
|
||||
|
||||
public function isLogDrainEnabled()
|
||||
{
|
||||
return data_get($this, 'is_log_drain_enabled', false);
|
||||
}
|
||||
|
||||
public function portsMappings(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
@@ -58,8 +63,9 @@ class StandaloneRedis extends BaseModel
|
||||
{
|
||||
return 'standalone-redis';
|
||||
}
|
||||
public function getDbUrl(): string {
|
||||
if ($this->is_public) {
|
||||
public function getDbUrl(bool $useInternal = false): string
|
||||
{
|
||||
if ($this->is_public && !$useInternal) {
|
||||
return "redis://:{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
|
||||
} else {
|
||||
return "redis://:{$this->redis_password}@{$this->uuid}:6379/0";
|
||||
|
||||
@@ -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', ''),
|
||||
|
||||
@@ -84,11 +84,14 @@ class DeploymentFailed extends Notification implements ShouldQueue
|
||||
} else {
|
||||
$message = 'Coolify: Deployment failed of **' . $this->application_name . '** (' . $this->fqdn . '): ';
|
||||
}
|
||||
$buttons[] = [
|
||||
"text" => "Deployment logs",
|
||||
"url" => $this->deployment_url
|
||||
];
|
||||
return [
|
||||
"message" => $message,
|
||||
"buttons" => [
|
||||
"text" => "View Deployment Logs",
|
||||
"url" => $this->deployment_url
|
||||
...$buttons
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -29,6 +29,10 @@ class EmailChannel
|
||||
->html((string)$mailMessage->render())
|
||||
);
|
||||
} catch (Exception $e) {
|
||||
$error = $e->getMessage();
|
||||
if ($error === 'No email settings found.') {
|
||||
throw $e;
|
||||
}
|
||||
ray($e->getMessage());
|
||||
$message = "EmailChannel error: {$e->getMessage()}. Failed to send email to:";
|
||||
if (isset($recepients)) {
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
65
app/Notifications/Server/HighDiskUsage.php
Normal file
65
app/Notifications/Server/HighDiskUsage.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications\Server;
|
||||
|
||||
use App\Models\Server;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use App\Notifications\Channels\DiscordChannel;
|
||||
use App\Notifications\Channels\EmailChannel;
|
||||
use App\Notifications\Channels\TelegramChannel;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class HighDiskUsage extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public $tries = 1;
|
||||
public function __construct(public Server $server, public int $disk_usage, public int $cleanup_after_percentage)
|
||||
{
|
||||
}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
$channels = [];
|
||||
$isEmailEnabled = isEmailEnabled($notifiable);
|
||||
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
|
||||
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
|
||||
|
||||
if ($isDiscordEnabled) {
|
||||
$channels[] = DiscordChannel::class;
|
||||
}
|
||||
if ($isEmailEnabled) {
|
||||
$channels[] = EmailChannel::class;
|
||||
}
|
||||
if ($isTelegramEnabled) {
|
||||
$channels[] = TelegramChannel::class;
|
||||
}
|
||||
return $channels;
|
||||
}
|
||||
|
||||
public function toMail(): MailMessage
|
||||
{
|
||||
$mail = new MailMessage();
|
||||
$mail->subject("Coolify: Server ({$this->server->name}) high disk usage detected!");
|
||||
$mail->view('emails.high-disk-usage', [
|
||||
'name' => $this->server->name,
|
||||
'disk_usage' => $this->disk_usage,
|
||||
'threshold' => $this->cleanup_after_percentage,
|
||||
]);
|
||||
return $mail;
|
||||
}
|
||||
|
||||
public function toDiscord(): string
|
||||
{
|
||||
$message = "Coolify: Server '{$this->server->name}' high disk usage detected!\nDisk usage: {$this->disk_usage}%. Threshold: {$this->cleanup_after_percentage}%.\nPlease cleanup your disk to prevent data-loss.\nHere are some tips: https://coolify.io/docs/automated-cleanup.";
|
||||
return $message;
|
||||
}
|
||||
public function toTelegram(): array
|
||||
{
|
||||
return [
|
||||
"message" => "Coolify: Server '{$this->server->name}' high disk usage detected!\nDisk usage: {$this->disk_usage}%. Threshold: {$this->cleanup_after_percentage}%.\nPlease cleanup your disk to prevent data-loss.\nHere are some tips: https://coolify.io/docs/automated-cleanup."
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ class Revived extends Notification implements ShouldQueue
|
||||
public $tries = 1;
|
||||
public function __construct(public Server $server)
|
||||
{
|
||||
if ($this->server->unreachable_email_sent === false) {
|
||||
if ($this->server->unreachable_notification_sent === false) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ use Illuminate\Support\Str;
|
||||
trait ExecuteRemoteCommand
|
||||
{
|
||||
public ?string $save = null;
|
||||
|
||||
public function execute_remote_command(...$commands)
|
||||
{
|
||||
static::$batch_counter++;
|
||||
@@ -32,16 +31,20 @@ trait ExecuteRemoteCommand
|
||||
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,
|
||||
|
||||
77
app/Traits/ExecuteRemoteCommandNew.php
Normal file
77
app/Traits/ExecuteRemoteCommandNew.php
Normal 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());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ class Input extends Component
|
||||
public bool $readonly = false,
|
||||
public string|null $helper = null,
|
||||
public bool $allowToPeak = true,
|
||||
public string $defaultClass = "input input-sm bg-coolgray-200 rounded text-white w-full disabled:bg-coolgray-200/50 disabled:border-none placeholder:text-coolgray-500 read-only:text-neutral-500 read-only:bg-coolgray-200/50"
|
||||
public string $defaultClass = "input input-sm bg-coolgray-100 rounded text-white w-full disabled:bg-coolgray-200/50 disabled:border-none placeholder:text-coolgray-500 read-only:text-neutral-500 read-only:bg-coolgray-200/50"
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ class Select extends Component
|
||||
public string|null $label = null,
|
||||
public string|null $helper = null,
|
||||
public bool $required = false,
|
||||
public string $defaultClass = "select select-sm w-full rounded text-white text-sm bg-coolgray-200 font-normal disabled:bg-coolgray-200/50 disabled:border-none"
|
||||
public string $defaultClass = "select select-sm w-full rounded text-white text-sm bg-coolgray-100 font-normal disabled:bg-coolgray-200/50 disabled:border-none"
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ class Textarea extends Component
|
||||
public bool $readonly = false,
|
||||
public string|null $helper = null,
|
||||
public bool $realtimeValidation = false,
|
||||
public string $defaultClass = "textarea leading-normal bg-coolgray-200 rounded text-white scrollbar disabled:bg-coolgray-200/50 disabled:border-none placeholder:text-coolgray-500 read-only:text-neutral-500 read-only:bg-coolgray-200/50"
|
||||
public string $defaultClass = "textarea leading-normal bg-coolgray-100 rounded text-white scrollbar disabled:bg-coolgray-200/50 disabled:border-none placeholder:text-coolgray-500 read-only:text-neutral-500 read-only:bg-coolgray-200/50"
|
||||
) {
|
||||
//
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,22 +16,28 @@ class Links extends Component
|
||||
{
|
||||
$this->links = collect([]);
|
||||
$service->applications()->get()->map(function ($application) {
|
||||
if ($application->fqdn) {
|
||||
$fqdns = collect(Str::of($application->fqdn)->explode(','));
|
||||
$fqdns->map(function ($fqdn) {
|
||||
$this->links->push(getFqdnWithoutPort($fqdn));
|
||||
});
|
||||
}
|
||||
if ($application->ports) {
|
||||
$portsCollection = collect(Str::of($application->ports)->explode(','));
|
||||
$portsCollection->map(function ($port) {
|
||||
if (Str::of($port)->contains(':')) {
|
||||
$hostPort = Str::of($port)->before(':');
|
||||
} else {
|
||||
$hostPort = $port;
|
||||
}
|
||||
$this->links->push(base_url(withPort:false) . ":{$hostPort}");
|
||||
});
|
||||
$type = $application->serviceType();
|
||||
if ($type) {
|
||||
$links = generateServiceSpecificFqdns($application, false);
|
||||
$this->links = $this->links->merge($links);
|
||||
} else {
|
||||
if ($application->fqdn) {
|
||||
$fqdns = collect(Str::of($application->fqdn)->explode(','));
|
||||
$fqdns->map(function ($fqdn) {
|
||||
$this->links->push(getFqdnWithoutPort($fqdn));
|
||||
});
|
||||
}
|
||||
if ($application->ports) {
|
||||
$portsCollection = collect(Str::of($application->ports)->explode(','));
|
||||
$portsCollection->map(function ($port) {
|
||||
if (Str::of($port)->contains(':')) {
|
||||
$hostPort = Str::of($port)->before(':');
|
||||
} else {
|
||||
$hostPort = $port;
|
||||
}
|
||||
$this->links->push(base_url(withPort: false) . ":{$hostPort}");
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
<?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)
|
||||
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)
|
||||
{
|
||||
$deployment = ApplicationDeploymentQueue::create([
|
||||
'application_id' => $application_id,
|
||||
@@ -14,6 +21,7 @@ function queue_application_deployment(int $application_id, string $deployment_uu
|
||||
'is_webhook' => $is_webhook,
|
||||
'restart_only' => $restart_only,
|
||||
'commit' => $commit,
|
||||
'git_type' => $git_type
|
||||
]);
|
||||
$queued_deployments = ApplicationDeploymentQueue::where('application_id', $application_id)->where('status', 'queued')->get()->sortByDesc('created_at');
|
||||
$running_deployments = ApplicationDeploymentQueue::where('application_id', $application_id)->where('status', 'in_progress')->get()->sortByDesc('created_at');
|
||||
@@ -28,17 +36,305 @@ function queue_application_deployment(int $application_id, string $deployment_uu
|
||||
if ($running_deployments->count() > 0) {
|
||||
return;
|
||||
}
|
||||
// New deployment
|
||||
// dispatchDeploymentJob($deployment->id);
|
||||
dispatch(new ApplicationDeploymentJob(
|
||||
application_deployment_queue_id: $deployment->id,
|
||||
))->onConnection('long-running')->onQueue('long-running');
|
||||
|
||||
}
|
||||
|
||||
function queue_next_deployment(Application $application)
|
||||
{
|
||||
$next_found = ApplicationDeploymentQueue::where('application_id', $application->id)->where('status', 'queued')->first();
|
||||
if ($next_found) {
|
||||
// New deployment
|
||||
// dispatchDeploymentJob($next_found->id);
|
||||
dispatch(new ApplicationDeploymentJob(
|
||||
application_deployment_queue_id: $next_found->id,
|
||||
))->onConnection('long-running')->onQueue('long-running');
|
||||
|
||||
}
|
||||
}
|
||||
function dispatchDeploymentJob($id)
|
||||
{
|
||||
$applicationQueue = ApplicationDeploymentQueue::find($id);
|
||||
$application = Application::find($applicationQueue->application_id);
|
||||
|
||||
$isRestartOnly = data_get($applicationQueue, 'restart_only');
|
||||
$isSimpleDockerFile = data_get($application, 'dockerfile');
|
||||
$isDockerImage = data_get($application, 'build_pack') === 'dockerimage';
|
||||
|
||||
if ($isRestartOnly) {
|
||||
ApplicationRestartJob::dispatch(applicationDeploymentQueueId: $id)->onConnection('long-running')->onQueue('long-running');
|
||||
} else if ($isSimpleDockerFile) {
|
||||
ApplicationDeploySimpleDockerfileJob::dispatch(applicationDeploymentQueueId: $id)->onConnection('long-running')->onQueue('long-running');
|
||||
} else if ($isDockerImage) {
|
||||
ApplicationDeployDockerImageJob::dispatch(applicationDeploymentQueueId: $id)->onConnection('long-running')->onQueue('long-running');
|
||||
} else {
|
||||
throw new Exception('Unknown build pack');
|
||||
}
|
||||
}
|
||||
|
||||
// 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 -v /:/host --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} -v /:/host --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) {
|
||||
ray('asd');
|
||||
$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 rollingUpdate(Application $application, string $deploymentUuid)
|
||||
{
|
||||
$commands = collect([]);
|
||||
$workDir = generateWorkdir($deploymentUuid, $application);
|
||||
if (count($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 {
|
||||
$commands->push(
|
||||
[
|
||||
"command" => "echo '\n----------------------------------------'"
|
||||
],
|
||||
[
|
||||
"command" => "echo -n 'Rolling update started.'"
|
||||
]
|
||||
);
|
||||
if ($application->build_pack === 'dockerimage') {
|
||||
$commands->push(
|
||||
["echo -n 'Pulling latest images from the registry.'"],
|
||||
[executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} pull"), "hidden" => true],
|
||||
[executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} up --build -d"), "hidden" => true],
|
||||
);
|
||||
} else {
|
||||
$commands->push(
|
||||
[executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} up --build -d"), "hidden" => true],
|
||||
);
|
||||
}
|
||||
return $commands;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?php
|
||||
|
||||
const REDACTED = '<REDACTED>';
|
||||
const DATABASE_TYPES = ['postgresql', 'redis', 'mongodb', 'mysql', 'mariadb'];
|
||||
const VALID_CRON_STRINGS = [
|
||||
'every_minute' => '* * * * *',
|
||||
@@ -23,3 +24,11 @@ const DATABASE_DOCKER_IMAGES = [
|
||||
'influxdb',
|
||||
'clickhouse/clickhouse-server'
|
||||
];
|
||||
const SPECIFIC_SERVICES = [
|
||||
'quay.io/minio/minio',
|
||||
];
|
||||
|
||||
const SUPPORTED_OS = [
|
||||
'debian',
|
||||
'rhel centos fedora'
|
||||
];
|
||||
|
||||
@@ -10,15 +10,22 @@ use Visus\Cuid2\Cuid2;
|
||||
|
||||
function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pullRequestId = null): Collection
|
||||
{
|
||||
if ($pullRequestId) {
|
||||
$containers = instant_remote_process(["docker ps -a --filter='label=coolify.applicationId={$id}' --filter='label=coolify.pullRequestId={$pullRequestId}' --format '{{json .}}' "], $server);
|
||||
} else {
|
||||
$containers = instant_remote_process(["docker ps -a --filter='label=coolify.applicationId={$id}' --format '{{json .}}'"], $server);
|
||||
}
|
||||
if (!$containers) {
|
||||
return collect([]);
|
||||
}
|
||||
return format_docker_command_output_to_json($containers);
|
||||
$containers = collect([]);
|
||||
$containers = instant_remote_process(["docker ps -a --filter='label=coolify.applicationId={$id}' --format '{{json .}}' "], $server);
|
||||
$containers = format_docker_command_output_to_json($containers);
|
||||
$containers = $containers->map(function ($container) use ($pullRequestId) {
|
||||
$labels = data_get($container, 'Labels');
|
||||
if (!str($labels)->contains("coolify.pullRequestId=")) {
|
||||
data_set($container, 'Labels', $labels . ",coolify.pullRequestId={$pullRequestId}");
|
||||
return $container;
|
||||
}
|
||||
if (str($labels)->contains("coolify.pullRequestId=$pullRequestId")) {
|
||||
return $container;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
$containers = $containers->filter();
|
||||
return $containers;
|
||||
}
|
||||
|
||||
function format_docker_command_output_to_json($rawOutput): Collection
|
||||
@@ -128,15 +135,49 @@ function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'applica
|
||||
$labels->push("coolify." . $type . "Id=" . $id);
|
||||
$labels->push("coolify.type=$type");
|
||||
$labels->push('coolify.name=' . $name);
|
||||
if ($pull_request_id !== 0) {
|
||||
$labels->push('coolify.pullRequestId=' . $pull_request_id);
|
||||
}
|
||||
$labels->push('coolify.pullRequestId=' . $pull_request_id);
|
||||
if ($type === 'service') {
|
||||
$labels->push('coolify.service.subId=' . $subId);
|
||||
$labels->push('coolify.service.subType=' . $subType);
|
||||
}
|
||||
return $labels;
|
||||
}
|
||||
function generateServiceSpecificFqdns($service, $forTraefik = false)
|
||||
{
|
||||
$variables = collect($service->service->environment_variables);
|
||||
$type = $service->serviceType();
|
||||
$payload = collect([]);
|
||||
switch ($type) {
|
||||
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)) {
|
||||
return $payload;
|
||||
}
|
||||
if (is_null($MINIO_BROWSER_REDIRECT_URL?->value)) {
|
||||
$MINIO_BROWSER_REDIRECT_URL?->update([
|
||||
"value" => generateFqdn($service->service->server, 'console-' . $service->uuid)
|
||||
]);
|
||||
}
|
||||
if (is_null($MINIO_SERVER_URL?->value)) {
|
||||
$MINIO_SERVER_URL?->update([
|
||||
"value" => generateFqdn($service->service->server, 'minio-' . $service->uuid)
|
||||
]);
|
||||
}
|
||||
if ($forTraefik) {
|
||||
$payload = collect([
|
||||
$MINIO_BROWSER_REDIRECT_URL->value . ':9001',
|
||||
$MINIO_SERVER_URL->value . ':9000',
|
||||
]);
|
||||
} else {
|
||||
$payload = collect([
|
||||
$MINIO_BROWSER_REDIRECT_URL->value,
|
||||
$MINIO_SERVER_URL->value,
|
||||
]);
|
||||
}
|
||||
}
|
||||
return $payload;
|
||||
}
|
||||
function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled, $onlyPort = null)
|
||||
{
|
||||
$labels = collect([]);
|
||||
|
||||
@@ -50,8 +50,11 @@ function generate_github_jwt_token(GithubApp $source)
|
||||
return $issuedToken;
|
||||
}
|
||||
|
||||
function githubApi(GithubApp|GitlabApp $source, string $endpoint, string $method = 'get', array|null $data = null, bool $throwError = true)
|
||||
function githubApi(GithubApp|GitlabApp|null $source, string $endpoint, string $method = 'get', array|null $data = null, bool $throwError = true)
|
||||
{
|
||||
if (is_null($source)) {
|
||||
throw new \Exception('Not implemented yet.');
|
||||
}
|
||||
if ($source->getMorphClass() == 'App\Models\GithubApp') {
|
||||
if ($source->is_public) {
|
||||
$response = Http::github($source->api_url)->$method($endpoint);
|
||||
|
||||
@@ -170,10 +170,13 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d
|
||||
$i['timestamp'] = Carbon::parse($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)) {
|
||||
@@ -191,7 +194,7 @@ function refresh_server_connection(?PrivateKey $private_key = null)
|
||||
// if (!$uptime) {
|
||||
// $server->settings->is_reachable = false;
|
||||
// $server->team->notify(new Unreachable($server));
|
||||
// $server->unreachable_email_sent = true;
|
||||
// $server->unreachable_notification_sent = true;
|
||||
// $server->save();
|
||||
// return [
|
||||
// "uptime" => null,
|
||||
@@ -213,9 +216,9 @@ function refresh_server_connection(?PrivateKey $private_key = null)
|
||||
// $server->settings->is_usable = false;
|
||||
// } else {
|
||||
// $server->settings->is_usable = true;
|
||||
// if (data_get($server, 'unreachable_email_sent') === true) {
|
||||
// if (data_get($server, 'unreachable_notification_sent') === true) {
|
||||
// $server->team->notify(new Revived($server));
|
||||
// $server->unreachable_email_sent = false;
|
||||
// $server->unreachable_notification_sent = false;
|
||||
// $server->save();
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -85,7 +85,6 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase $oneS
|
||||
} else {
|
||||
$fileLocation = $path;
|
||||
}
|
||||
ray($path,$fileLocation);
|
||||
// Exists and is a file
|
||||
$isFile = instant_remote_process(["test -f $fileLocation && echo OK || echo NOK"], $server);
|
||||
// Exists and is a directory
|
||||
@@ -135,19 +134,21 @@ function updateCompose($resource)
|
||||
$image = data_get($resource, 'image');
|
||||
data_set($dockerCompose, "services.{$name}.image", $image);
|
||||
|
||||
// Update FQDN
|
||||
$variableName = "SERVICE_FQDN_" . Str::of($resource->name)->upper();
|
||||
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
|
||||
if ($generatedEnv) {
|
||||
$generatedEnv->value = $resource->fqdn;
|
||||
$generatedEnv->save();
|
||||
}
|
||||
$variableName = "SERVICE_URL_" . Str::of($resource->name)->upper();
|
||||
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
|
||||
if ($generatedEnv) {
|
||||
$url = Str::of($resource->fqdn)->after('://');
|
||||
$generatedEnv->value = $url;
|
||||
$generatedEnv->save();
|
||||
if (!str($resource->fqdn)->contains(',')) {
|
||||
// Update FQDN
|
||||
$variableName = "SERVICE_FQDN_" . Str::of($resource->name)->upper();
|
||||
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
|
||||
if ($generatedEnv) {
|
||||
$generatedEnv->value = $resource->fqdn;
|
||||
$generatedEnv->save();
|
||||
}
|
||||
$variableName = "SERVICE_URL_" . Str::of($resource->name)->upper();
|
||||
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
|
||||
if ($generatedEnv) {
|
||||
$url = Str::of($resource->fqdn)->after('://');
|
||||
$generatedEnv->value = $url;
|
||||
$generatedEnv->save();
|
||||
}
|
||||
}
|
||||
|
||||
$dockerComposeRaw = Yaml::dump($dockerCompose, 10, 2);
|
||||
|
||||
@@ -27,7 +27,6 @@ use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Stringable;
|
||||
use Nubs\RandomNameGenerator\All;
|
||||
use Poliander\Cron\CronExpression;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
use phpseclib3\Crypt\RSA;
|
||||
@@ -94,8 +93,13 @@ function refreshSession(?Team $team = null): void
|
||||
}
|
||||
function handleError(?Throwable $error = null, ?Livewire\Component $livewire = null, ?string $customErrorMessage = null)
|
||||
{
|
||||
ray('handleError');
|
||||
ray($error);
|
||||
if ($error instanceof TooManyRequestsException) {
|
||||
if (isset($livewire)) {
|
||||
return $livewire->emit('error', "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds.");
|
||||
}
|
||||
return "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds.";
|
||||
}
|
||||
|
||||
if ($error instanceof Throwable) {
|
||||
$message = $error->getMessage();
|
||||
} else {
|
||||
@@ -104,55 +108,12 @@ function handleError(?Throwable $error = null, ?Livewire\Component $livewire = n
|
||||
if ($customErrorMessage) {
|
||||
$message = $customErrorMessage . ' ' . $message;
|
||||
}
|
||||
if ($error instanceof TooManyRequestsException) {
|
||||
if (isset($livewire)) {
|
||||
return $livewire->emit('error', "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds.");
|
||||
}
|
||||
return "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds.";
|
||||
}
|
||||
|
||||
if (isset($livewire)) {
|
||||
return $livewire->emit('error', $message);
|
||||
}
|
||||
|
||||
throw new RuntimeException($message);
|
||||
throw new Exception($message);
|
||||
}
|
||||
function general_error_handler(Throwable $err, Livewire\Component $that = null, $isJson = false, $customErrorMessage = null): mixed
|
||||
{
|
||||
try {
|
||||
ray($err);
|
||||
ray('ERROR OCCURRED: ' . $err->getMessage());
|
||||
if ($err instanceof QueryException) {
|
||||
if ($err->errorInfo[0] === '23505') {
|
||||
throw new Exception($customErrorMessage ?? 'Duplicate entry found.', '23505');
|
||||
} else if (count($err->errorInfo) === 4) {
|
||||
throw new Exception($customErrorMessage ?? $err->errorInfo[3]);
|
||||
} else {
|
||||
throw new Exception($customErrorMessage ?? $err->errorInfo[2]);
|
||||
}
|
||||
} elseif ($err instanceof TooManyRequestsException) {
|
||||
throw new Exception($customErrorMessage ?? "Too many requests. Please try again in {$err->secondsUntilAvailable} seconds.");
|
||||
} else {
|
||||
if ($err->getMessage() === 'This action is unauthorized.') {
|
||||
return redirect()->route('dashboard')->with('error', $customErrorMessage ?? $err->getMessage());
|
||||
}
|
||||
throw new Exception($customErrorMessage ?? $err->getMessage());
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
if ($that) {
|
||||
return $that->emit('error', $customErrorMessage ?? $e->getMessage());
|
||||
} elseif ($isJson) {
|
||||
return response()->json([
|
||||
'code' => $e->getCode(),
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
} else {
|
||||
ray($customErrorMessage);
|
||||
ray($e);
|
||||
return $customErrorMessage ?? $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function get_route_parameters(): array
|
||||
{
|
||||
return Route::current()->parameters();
|
||||
@@ -173,7 +134,11 @@ function get_latest_version_of_coolify(): string
|
||||
|
||||
function generate_random_name(?string $cuid = null): string
|
||||
{
|
||||
$generator = All::create();
|
||||
$generator = new \Nubs\RandomNameGenerator\All(
|
||||
[
|
||||
new \Nubs\RandomNameGenerator\Alliteration(),
|
||||
]
|
||||
);
|
||||
if (is_null($cuid)) {
|
||||
$cuid = new Cuid2(7);
|
||||
}
|
||||
@@ -444,20 +409,25 @@ function getServiceTemplates()
|
||||
if (isDev()) {
|
||||
$services = File::get(base_path('templates/service-templates.json'));
|
||||
$services = collect(json_decode($services))->sortKeys();
|
||||
$version = config('version');
|
||||
$services = $services->map(function ($service) use ($version) {
|
||||
if (version_compare($version, data_get($service, 'minVersion', '0.0.0'), '<')) {
|
||||
$service->disabled = true;
|
||||
}
|
||||
return $service;
|
||||
});
|
||||
} else {
|
||||
$services = Http::get(config('constants.services.official'));
|
||||
if ($services->failed()) {
|
||||
throw new \Exception($services->body());
|
||||
try {
|
||||
$response = Http::retry(3, 50)->get(config('constants.services.official'));
|
||||
if ($response->failed()) {
|
||||
return collect([]);
|
||||
}
|
||||
$services = $response->json();
|
||||
$services = collect($services)->sortKeys();
|
||||
} catch (\Throwable $e) {
|
||||
$services = collect([]);
|
||||
}
|
||||
$services = collect($services->json())->sortKeys();
|
||||
}
|
||||
// $version = config('version');
|
||||
// $services = $services->map(function ($service) use ($version) {
|
||||
// if (version_compare($version, data_get($service, 'minVersion', '0.0.0'), '<')) {
|
||||
// $service->disabled = true;
|
||||
// }
|
||||
// return $service;
|
||||
// });
|
||||
return $services;
|
||||
}
|
||||
|
||||
@@ -493,7 +463,8 @@ function queryResourcesByUuid(string $uuid)
|
||||
return $resource;
|
||||
}
|
||||
|
||||
function generateDeployWebhook($resource) {
|
||||
function generateDeployWebhook($resource)
|
||||
{
|
||||
$baseUrl = base_url();
|
||||
$api = Url::fromString($baseUrl) . '/api/v1';
|
||||
$endpoint = '/deploy';
|
||||
@@ -501,3 +472,18 @@ function generateDeployWebhook($resource) {
|
||||
$url = $api . $endpoint . "?uuid=$uuid&force=false";
|
||||
return $url;
|
||||
}
|
||||
function generateGitManualWebhook($resource, $type) {
|
||||
if ($resource->source_id !== 0 && !is_null($resource->source_id)) {
|
||||
return null;
|
||||
}
|
||||
if ($resource->getMorphClass() === 'App\Models\Application') {
|
||||
$baseUrl = base_url();
|
||||
$api = Url::fromString($baseUrl) . "/webhooks/source/$type/events/manual";
|
||||
return $api;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function removeAnsiColors($text)
|
||||
{
|
||||
return preg_replace('/\e[[][A-Za-z0-9];?[0-9]*m?/', '', $text);
|
||||
}
|
||||
|
||||
743
composer.lock
generated
743
composer.lock
generated
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user