Compare commits

...

100 Commits

Author SHA1 Message Date
Andras Bacsai
59d6818f70 Merge pull request #1339 from coollabsio/next
v4.0.0-beta.93
2023-10-18 11:30:40 +02:00
Andras Bacsai
7678cd47df fix: add config_hash if its null (old deployments) 2023-10-18 11:26:01 +02:00
Andras Bacsai
b101fbacd4 fix: do not show configuration changed if config_hash is null 2023-10-18 11:22:56 +02:00
Andras Bacsai
a61a86dc3b feat: show if config is not applied 2023-10-18 11:20:40 +02:00
Andras Bacsai
0b3cde44c3 feat: able to customize docker labels on applications 2023-10-18 10:32:08 +02:00
Andras Bacsai
6b302ab786 Add restarting indicator to resources 2023-10-18 09:03:14 +02:00
Andras Bacsai
3c623f13e2 revert version 2023-10-17 20:54:54 +02:00
Andras Bacsai
da54c24e8d fix: setup:dev script & contribution guide 2023-10-17 20:54:26 +02:00
Andras Bacsai
1e39c3d5ab Merge pull request #1338 from coollabsio/next
v4.0.0-beta.92
2023-10-17 19:03:04 +02:00
Andras Bacsai
6071412986 fix: proxy start process 2023-10-17 19:00:23 +02:00
Andras Bacsai
ba7148206a Merge pull request #1336 from coollabsio/next
v4.0.0-beta.91
2023-10-17 15:41:30 +02:00
Andras Bacsai
59c5b22e6c fix: always start proxy if not NONE is selected 2023-10-17 15:40:47 +02:00
Andras Bacsai
be7f2ad9c4 ui: add helper to service domains 2023-10-17 15:34:20 +02:00
Andras Bacsai
62295ef573 Merge pull request #1335 from coollabsio/next
v4.0.0-beta.90
2023-10-17 14:45:26 +02:00
Andras Bacsai
ceb9fcf3b6 service: wordpress 2023-10-17 14:44:25 +02:00
Andras Bacsai
60282f7b6c fix: only include config.json if its exists and a file 2023-10-17 14:23:07 +02:00
Andras Bacsai
f14b0a3411 Merge pull request #1334 from coollabsio/next
v4.0.0-beta.89
2023-10-17 14:06:12 +02:00
Andras Bacsai
30af317bd9 fix: show docker build logs 2023-10-17 14:04:21 +02:00
Andras Bacsai
95faa1c3ad fix: noindex meta tag 2023-10-17 13:28:33 +02:00
Andras Bacsai
fb280afe41 Merge pull request #1332 from coollabsio/next
v4.0.0-beta.88
2023-10-17 12:41:45 +02:00
Andras Bacsai
fd488a561a feat: use docker login credentials from server 2023-10-17 12:35:04 +02:00
Andras Bacsai
ab57a5d8ef Merge pull request #1331 from coollabsio/next
v4.0.0-beta.87
2023-10-17 12:14:03 +02:00
Andras Bacsai
f5ae222a6e fix: add internal domain names during build process 2023-10-17 11:23:49 +02:00
Andras Bacsai
5d95d8b79a fix: cancel any deployments + queue next 2023-10-17 11:10:33 +02:00
Andras Bacsai
fbb5f2ca2e fix: generate fqdn if you deleted a service app, but it requires fqdn 2023-10-17 10:37:39 +02:00
Andras Bacsai
16cbca36c1 add trademark policy 2023-10-17 10:37:26 +02:00
Andras Bacsai
24a578bedb Merge pull request #1327 from theh2so4/main
[+] Update
2023-10-17 10:31:25 +02:00
Andras Bacsai
36dc479772 fix: service status check is a bit better 2023-10-17 10:17:03 +02:00
Andras Bacsai
83d6e488e4 Merge pull request #1330 from seii/fix/raspbian-support
Add Raspbian support to install.sh
2023-10-17 09:50:57 +02:00
Seii
a4d358d512 Add Raspbian support to install.sh 2023-10-16 22:11:51 -06:00
TheH2SO4
c0c197101d [+] Update
💄 **Styling**:

-> ℹ️ **Alphabetical order**: Changed the order of the templates and set them on a alphabetical order.
-> ℹ️ **More accurate descriptions**: Created more accurate descriptions for the templates: `Appsmith, Appwrite, Fider, Ghost, Umami` and `Uptime Kuma`.
2023-10-15 23:48:33 +02:00
Andras Bacsai
62e39ccc7f Merge pull request #1326 from coollabsio/next
v4.0.0-beta.86
2023-10-15 16:56:21 +02:00
Andras Bacsai
a88a016137 fix: build image before starting dockerfile buildpacks 2023-10-15 16:54:16 +02:00
Andras Bacsai
f4c8986ab3 Merge pull request #1324 from coollabsio/next
v4.0.0-beta.85
2023-10-14 17:40:08 +02:00
Andras Bacsai
e286eae53b Merge pull request #1322 from scmmishra/fix/redis-default-url
fix: generated redis URL string
2023-10-14 17:36:48 +02:00
Andras Bacsai
bc3e59e4ef fix: delete resource if there was an error
fix: do not refresh on containerStatusJob (db view)
2023-10-14 17:26:16 +02:00
Andras Bacsai
17ebc650c9 Merge pull request #1323 from coollabsio/next
v4.0.0-beta.84
2023-10-14 14:24:20 +02:00
Andras Bacsai
0ef386b4a8 fix: stopping a resource is now job based
ui: show status on project
2023-10-14 14:22:07 +02:00
Shivam Mishra
26fce85bb0 fix: redis URL generated 2023-10-14 12:10:12 +05:30
Andras Bacsai
2a079e3365 Merge pull request #1321 from coollabsio/next
v4.0.0-beta.83
2023-10-13 21:54:26 +02:00
Andras Bacsai
5fb5ed75c4 Merge pull request #1315 from scmmishra/patch-1
fix: docker hub URL for redis
2023-10-13 21:46:46 +02:00
Andras Bacsai
a2008fe9d1 typo 2023-10-13 21:44:04 +02:00
Andras Bacsai
3c96485e3d fix: custom dockerfile location for dockerfile buildpack
fix: able to use custom port for git cloning
2023-10-13 21:39:49 +02:00
Andras Bacsai
1c97d47ea0 fix: turn off static deployment if you switch buildpacks 2023-10-13 21:08:59 +02:00
Shivam Mishra
8f8f5878dd fix: docker hub URL 2023-10-13 22:34:46 +05:30
Andras Bacsai
6e6f39dc1f Merge pull request #1314 from coollabsio/next
v4.0.0-beta.82
2023-10-13 16:38:52 +02:00
Andras Bacsai
d2d1f984e1 fix 2023-10-13 16:32:59 +02:00
Andras Bacsai
d635e5dbae fix: backup database one-by-one. 2023-10-13 15:45:24 +02:00
Andras Bacsai
49c56524e1 fix: dev containerjobs 2023-10-13 15:17:04 +02:00
Andras Bacsai
6ced607f2a fix: timeout for instant remote processes 2023-10-13 15:16:52 +02:00
Andras Bacsai
aaa2febef4 fix: docker cleanup jobs 2023-10-13 15:16:33 +02:00
Andras Bacsai
10e6eddcfe fix: db labels 2023-10-13 15:16:22 +02:00
Andras Bacsai
2639bf92ad fix: traefik dashboard ip 2023-10-13 14:35:02 +02:00
Andras Bacsai
59eae3a44e fix: proxy check for ports, do not kill anything listening on port 80/443 2023-10-13 14:25:30 +02:00
Andras Bacsai
5aa8ccfcf4 link to default redis conf 2023-10-13 09:42:38 +02:00
Andras Bacsai
aea7cc9638 fix: show database logs in case of its not healthy and running 2023-10-13 09:40:37 +02:00
Andras Bacsai
c970907c73 fix: no backup for redis 2023-10-13 09:39:40 +02:00
Andras Bacsai
38c6c1ee40 fix: urls should be password fields 2023-10-13 09:36:37 +02:00
Andras Bacsai
a6118f5daf fix 2023-10-13 09:34:11 +02:00
Andras Bacsai
b196c138d9 fix: server ip could be hostname in self-hosted 2023-10-13 09:31:44 +02:00
Andras Bacsai
8f9949160c feat: add custom redis conf 2023-10-12 17:29:29 +02:00
Andras Bacsai
beae0b545f init: redis 2023-10-12 17:18:33 +02:00
Andras Bacsai
b8dd7704b3 update resource delete command 2023-10-12 13:35:57 +02:00
Andras Bacsai
d913be66e6 Merge pull request #1312 from coollabsio/next
v4.0.0-beta.81
2023-10-12 12:23:03 +02:00
Andras Bacsai
63de538879 seeder update 2023-10-12 12:18:26 +02:00
Andras Bacsai
1d733b2282 seeder 2023-10-12 12:11:54 +02:00
Andras Bacsai
1d0ad51fdf fix: dockerfile location feature 2023-10-12 12:01:09 +02:00
Andras Bacsai
83d00bbe3c fix: make sure to use IP address 2023-10-12 11:41:37 +02:00
Andras Bacsai
c422b4dbcf fix: isCloud in production seeder 2023-10-12 11:36:44 +02:00
Andras Bacsai
767fd334dd Merge pull request #1310 from coollabsio/next
v4.0.0-beta.80
2023-10-12 09:47:39 +02:00
Andras Bacsai
972223f01b disable docker_compose deployments 2023-10-12 09:30:27 +02:00
Andras Bacsai
9318cac189 fix: service status check
fix: containerStatusJob
fix: service form
2023-10-12 09:12:46 +02:00
Andras Bacsai
7aa991fd7c fix: service check status 10 sec 2023-10-12 08:58:08 +02:00
Andras Bacsai
5c27f43b3d move autoupdate job to actions 2023-10-12 08:56:29 +02:00
Andras Bacsai
a2f4d4ed6d fix: make sure proxy wont start in NONE mode 2023-10-12 08:51:32 +02:00
Andras Bacsai
6aca2740fb fix 2023-10-11 15:46:59 +02:00
Andras Bacsai
cd13b5b83e version++ 2023-10-11 15:39:27 +02:00
Andras Bacsai
758dbafbf1 Merge pull request #1308 from coollabsio/next
v4.0.0-beta.79
2023-10-11 15:27:21 +02:00
Andras Bacsai
f6663661df disallow robots 2023-10-11 15:07:00 +02:00
Andras Bacsai
9666099408 commit 2023-10-11 14:43:34 +02:00
Andras Bacsai
d382af6860 fix 2023-10-11 14:40:41 +02:00
Andras Bacsai
4905454269 hm 2023-10-11 14:33:18 +02:00
Andras Bacsai
ed8bd37230 disallow robots 2023-10-11 14:31:59 +02:00
Andras Bacsai
ec1a7aa893 Merge pull request #1307 from coollabsio/next
v4.0.0-beta.78
2023-10-11 14:25:27 +02:00
Andras Bacsai
62adf2c5dc fix: boarding + verification 2023-10-11 14:24:19 +02:00
Andras Bacsai
3e4538de98 update command 2023-10-11 14:12:02 +02:00
Andras Bacsai
5a7b16ea5f command: delete server 2023-10-11 14:04:21 +02:00
Andras Bacsai
aa7bc40f85 fix: send unreachable/revived notifications 2023-10-11 13:52:46 +02:00
Andras Bacsai
4fd83dc727 version++ 2023-10-11 13:51:30 +02:00
Andras Bacsai
0e451f87a9 Merge pull request #1305 from coollabsio/next
v4.0.0-beta.77
2023-10-11 13:50:56 +02:00
Andras Bacsai
0b0ae55f0b fix 2023-10-11 13:47:14 +02:00
Andras Bacsai
40ec3d9753 fix 2023-10-11 13:34:51 +02:00
Andras Bacsai
9535c8df29 fix: check localhost connection 2023-10-11 13:30:36 +02:00
Andras Bacsai
6ca1d36d5d fix: cannot remove localhost 2023-10-11 13:12:29 +02:00
Andras Bacsai
f5d16c46cb version++ 2023-10-11 13:11:52 +02:00
Andras Bacsai
725c3fd547 Merge pull request #1304 from coollabsio/next
v4.0.0-beta.76
2023-10-11 12:57:35 +02:00
Andras Bacsai
dcfcee1db6 fix: public git 2023-10-11 12:56:57 +02:00
Andras Bacsai
9f8c44d96b version++ 2023-10-11 12:33:06 +02:00
Andras Bacsai
5b74fd34f5 fix: instant save build pack change 2023-10-11 12:12:25 +02:00
Andras Bacsai
5a4c9422b2 fix: only require registry image in case of dockerimage bp 2023-10-11 12:10:40 +02:00
103 changed files with 1751 additions and 630 deletions

View File

@@ -27,3 +27,5 @@ You can ask for guidance anytime on our
You can login your Coolify instance at `localhost:8000` with `test@example.com` and `password`. You can login your Coolify instance at `localhost:8000` with `test@example.com` and `password`.
Your horizon (Laravel scheduler): `localhost:8000/horizon` - Only reachable if you logged in with root user. Your horizon (Laravel scheduler): `localhost:8000/horizon` - Only reachable if you logged in with root user.
Mails are caught by Mailpit: `localhost:8025`

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Actions\Application;
use App\Models\Application;
use App\Notifications\Application\StatusChanged;
use Lorisleiva\Actions\Concerns\AsAction;
class StopApplication
{
use AsAction;
public function handle(Application $application)
{
$server = $application->destination->server;
$containers = getCurrentApplicationContainerStatus($server, $application->id);
if ($containers->count() > 0) {
foreach ($containers as $container) {
$containerName = data_get($container, 'Names');
if ($containerName) {
instant_remote_process(
["docker rm -f {$containerName}"],
$server
);
}
}
// TODO: make notification for application
// $application->environment->project->team->notify(new StatusChanged($application));
}
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace App\Actions\Database;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml;
class StartDatabaseProxy
{
use AsAction;
public function handle(StandaloneRedis|StandalonePostgresql $database)
{
$internalPort = null;
if ($database->getMorphClass()=== 'App\Models\StandaloneRedis') {
$internalPort = 6379;
} else if ($database->getMorphClass()=== 'App\Models\StandalonePostgresql') {
$internalPort = 5432;
}
$containerName = "{$database->uuid}-proxy";
$configuration_dir = database_proxy_dir($database->uuid);
$nginxconf = <<<EOF
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
events {
worker_connections 1024;
}
stream {
server {
listen $database->public_port;
proxy_pass $database->uuid:$internalPort;
}
}
EOF;
$dockerfile = <<< EOF
FROM nginx:stable-alpine
COPY nginx.conf /etc/nginx/nginx.conf
EOF;
$docker_compose = [
'version' => '3.8',
'services' => [
$containerName => [
'build' => [
'context' => $configuration_dir,
'dockerfile' => 'Dockerfile',
],
'image' => "nginx:stable-alpine",
'container_name' => $containerName,
'restart' => RESTART_MODE,
'ports' => [
"$database->public_port:$database->public_port",
],
'networks' => [
$database->destination->network,
],
'healthcheck' => [
'test' => [
'CMD-SHELL',
'stat /etc/nginx/nginx.conf || exit 1',
],
'interval' => '5s',
'timeout' => '5s',
'retries' => 3,
'start_period' => '1s'
],
]
],
'networks' => [
$database->destination->network => [
'external' => true,
'name' => $database->destination->network,
'attachable' => true,
]
]
];
$dockercompose_base64 = base64_encode(Yaml::dump($docker_compose, 4, 2));
$nginxconf_base64 = base64_encode($nginxconf);
$dockerfile_base64 = base64_encode($dockerfile);
instant_remote_process([
"mkdir -p $configuration_dir",
"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} up --build -d",
], $database->destination->server);
}
}

View File

@@ -6,15 +6,18 @@ use App\Models\Server;
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
use Lorisleiva\Actions\Concerns\AsAction;
class StartPostgresql class StartPostgresql
{ {
use AsAction;
public StandalonePostgresql $database; public StandalonePostgresql $database;
public array $commands = []; public array $commands = [];
public array $init_scripts = []; public array $init_scripts = [];
public string $configuration_dir; public string $configuration_dir;
public function __invoke(Server $server, StandalonePostgresql $database) public function handle(Server $server, StandalonePostgresql $database)
{ {
$this->database = $database; $this->database = $database;
$container_name = $this->database->uuid; $container_name = $this->database->uuid;
@@ -41,6 +44,9 @@ class StartPostgresql
'networks' => [ 'networks' => [
$this->database->destination->network, $this->database->destination->network,
], ],
'labels' => [
'coolify.managed' => 'true',
],
'healthcheck' => [ 'healthcheck' => [
'test' => [ 'test' => [
'CMD-SHELL', 'CMD-SHELL',

View File

@@ -0,0 +1,160 @@
<?php
namespace App\Actions\Database;
use App\Models\Server;
use App\Models\StandaloneRedis;
use Illuminate\Support\Str;
use Symfony\Component\Yaml\Yaml;
use Lorisleiva\Actions\Concerns\AsAction;
class StartRedis
{
use AsAction;
public StandaloneRedis $database;
public array $commands = [];
public string $configuration_dir;
public function handle(Server $server, StandaloneRedis $database)
{
$this->database = $database;
$startCommand = "redis-server --requirepass {$this->database->redis_password} --appendonly yes";
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
"echo '####### Starting {$database->name}.'",
"mkdir -p $this->configuration_dir",
];
$persistent_storages = $this->generate_local_persistent_volumes();
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
$environment_variables = $this->generate_environment_variables();
$this->add_custom_redis();
$docker_compose = [
'version' => '3.8',
'services' => [
$container_name => [
'image' => $this->database->image,
'command' => $startCommand,
'container_name' => $container_name,
'environment' => $environment_variables,
'restart' => RESTART_MODE,
'networks' => [
$this->database->destination->network,
],
'labels' => [
'coolify.managed' => 'true',
],
'healthcheck' => [
'test' => [
'CMD-SHELL',
'redis-cli',
'ping'
],
'interval' => '5s',
'timeout' => '5s',
'retries' => 10,
'start_period' => '5s'
],
'mem_limit' => $this->database->limits_memory,
'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,
'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares,
]
],
'networks' => [
$this->database->destination->network => [
'external' => true,
'name' => $this->database->destination->network,
'attachable' => true,
]
]
];
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
}
if (count($persistent_storages) > 0) {
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
}
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
if (!is_null($this->database->redis_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
'source' => $this->configuration_dir . '/redis.conf',
'target' => '/usr/local/etc/redis/redis.conf',
'read_only' => true,
];
$docker_compose['services'][$container_name]['command'] = $startCommand . ' /usr/local/etc/redis/redis.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[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo '####### {$database->name} started.'";
return remote_process($this->commands, $server);
}
private function generate_local_persistent_volumes()
{
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
$volume_name = $persistentStorage->host_path ?? $persistentStorage->name;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
return $local_persistent_volumes;
}
private function generate_local_persistent_volumes_only_volume_names()
{
$local_persistent_volumes_names = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path) {
continue;
}
$name = $persistentStorage->name;
$local_persistent_volumes_names[$name] = [
'name' => $name,
'external' => false,
];
}
return $local_persistent_volumes_names;
}
private function generate_environment_variables()
{
$environment_variables = collect();
foreach ($this->database->runtime_environment_variables as $env) {
$environment_variables->push("$env->key=$env->value");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
$environment_variables->push("REDIS_PASSWORD={$this->database->redis_password}");
}
return $environment_variables->all();
}
private function add_custom_redis()
{
if (is_null($this->database->redis_conf)) {
return;
}
$filename = 'redis.conf';
$content = $this->database->redis_conf;
$content_base64 = base64_encode($content);
$this->commands[] = "echo '{$content_base64}' | base64 -d > $this->configuration_dir/{$filename}";
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Actions\Database;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use App\Notifications\Application\StatusChanged;
use Lorisleiva\Actions\Concerns\AsAction;
class StopDatabase
{
use AsAction;
public function handle(StandaloneRedis|StandalonePostgresql $database)
{
$server = $database->destination->server;
instant_remote_process(
["docker rm -f {$database->uuid}"],
$server
);
if ($database->is_public) {
StopDatabaseProxy::run($database);
}
// TODO: make notification for services
// $database->environment->project->team->notify(new StatusChanged($database));
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Actions\Database;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Lorisleiva\Actions\Concerns\AsAction;
class StopDatabaseProxy
{
use AsAction;
public function handle(StandaloneRedis|StandalonePostgresql $database)
{
instant_remote_process(["docker rm -f {$database->uuid}-proxy"], $database->destination->server);
$database->is_public = false;
$database->save();
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Actions\Proxy;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
class CheckProxy
{
use AsAction;
public function handle(Server $server, $fromUI = false)
{
if (!$server->isProxyShouldRun()) {
if ($fromUI) {
throw new \Exception("Proxy should not run. You selected the Custom Proxy.");
} else {
return false;
}
}
$status = getContainerStatus($server, 'coolify-proxy');
if ($status === 'running') {
$server->proxy->set('status', 'running');
$server->save();
return false;
}
$ip = $server->ip;
if ($server->id === 0) {
$ip = 'host.docker.internal';
}
$connection80 = @fsockopen($ip, '80');
$connection443 = @fsockopen($ip, '443');
$port80 = is_resource($connection80) && fclose($connection80);
$port443 = is_resource($connection443) && fclose($connection443);
if ($port80) {
if ($fromUI) {
throw new \Exception("Port 80 is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a> <br> Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>");
} else {
return false;
}
}
if ($port443) {
if ($fromUI) {
throw new \Exception("Port 443 is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a> <br> Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>");
} else {
return false;
}
}
return true;
}
}

View File

@@ -10,56 +10,47 @@ use Spatie\Activitylog\Models\Activity;
class StartProxy class StartProxy
{ {
use AsAction; use AsAction;
public function handle(Server $server, bool $async = true): Activity|string public function handle(Server $server, bool $async = true): string|Activity
{ {
$commands = collect([]); try {
$proxyType = $server->proxyType(); $proxyType = $server->proxyType();
if ($proxyType === 'none') { $commands = collect([]);
return 'OK'; $proxy_path = get_proxy_path();
} $configuration = CheckConfiguration::run($server);
$proxy_path = get_proxy_path(); if (!$configuration) {
$configuration = CheckConfiguration::run($server); throw new \Exception("Configuration is not synced");
if (!$configuration) { }
throw new \Exception("Configuration is not synced"); SaveConfiguration::run($server, $configuration);
} $docker_compose_yml_base64 = base64_encode($configuration);
SaveConfiguration::run($server, $configuration); $server->proxy->last_applied_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value;
$docker_compose_yml_base64 = base64_encode($configuration);
$server->proxy->last_applied_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value;
$server->save();
$commands = $commands->merge([
"apt-get update > /dev/null 2>&1 || true",
"command -v lsof >/dev/null || echo '####### Installing lsof.'",
"command -v lsof >/dev/null || apt install -y lsof",
"command -v lsof >/dev/null || command -v fuser >/dev/null || apt install -y psmisc",
"mkdir -p $proxy_path && cd $proxy_path",
"echo '####### Creating Docker Compose file.'",
"echo '####### Pulling docker image.'",
'docker compose pull',
"echo '####### Stopping existing coolify-proxy.'",
"docker compose down -v --remove-orphans > /dev/null 2>&1",
"command -v fuser >/dev/null || command -v lsof >/dev/null || echo '####### Could not kill existing processes listening on port 80 & 443. Please stop the process holding these ports...'",
"command -v lsof >/dev/null && lsof -nt -i:80 | xargs -r kill -9 || true",
"command -v lsof >/dev/null && lsof -nt -i:443 | xargs -r kill -9 || true",
"command -v fuser >/dev/null && fuser -k 80/tcp || true",
"command -v fuser >/dev/null && fuser -k 443/tcp || true",
"systemctl disable nginx > /dev/null 2>&1 || true",
"systemctl disable apache2 > /dev/null 2>&1 || true",
"systemctl disable apache > /dev/null 2>&1 || true",
"echo '####### Starting coolify-proxy.'",
'docker compose up -d --remove-orphans',
"echo '####### Proxy installed successfully.'"
]);
$commands = $commands->merge(connectProxyToNetworks($server));
if ($async) {
$activity = remote_process($commands, $server);
return $activity;
} else {
instant_remote_process($commands, $server);
$server->proxy->set('status', 'running');
$server->proxy->set('type', $proxyType);
$server->save(); $server->save();
return 'OK'; $commands = $commands->merge([
"mkdir -p $proxy_path && cd $proxy_path",
"echo 'Creating required Docker Compose file.'",
"echo 'Pulling docker image.'",
'docker compose pull',
"echo 'Stopping existing coolify-proxy.'",
"docker compose down -v --remove-orphans > /dev/null 2>&1",
"echo 'Starting coolify-proxy.'",
'docker compose up -d --remove-orphans',
"echo 'Proxy started successfully.'"
]);
$commands = $commands->merge(connectProxyToNetworks($server));
if ($async) {
$activity = remote_process($commands, $server);
return $activity;
} else {
instant_remote_process($commands, $server);
$server->proxy->set('status', 'running');
$server->proxy->set('type', $proxyType);
$server->save();
return 'OK';
}
} catch(\Throwable $e) {
ray($e);
throw $e;
} }
} }
} }

View File

@@ -2,16 +2,18 @@
namespace App\Actions\Server; namespace App\Actions\Server;
use Lorisleiva\Actions\Concerns\AsAction;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use App\Models\Server; use App\Models\Server;
class UpdateCoolify class UpdateCoolify
{ {
use AsAction;
public ?Server $server = null; public ?Server $server = null;
public ?string $latestVersion = null; public ?string $latestVersion = null;
public ?string $currentVersion = null; public ?string $currentVersion = null;
public function __invoke(bool $force) public function handle(bool $force)
{ {
try { try {
$settings = InstanceSettings::get(); $settings = InstanceSettings::get();

View File

@@ -4,6 +4,7 @@ namespace App\Actions\Service;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
use App\Models\Service; use App\Models\Service;
use App\Notifications\Application\StatusChanged;
class StopService class StopService
{ {
@@ -22,5 +23,7 @@ class StopService
} }
instant_remote_process(["docker network disconnect {$service->uuid} coolify-proxy 2>/dev/null"], $service->server, false); instant_remote_process(["docker network disconnect {$service->uuid} coolify-proxy 2>/dev/null"], $service->server, false);
instant_remote_process(["docker network rm {$service->uuid} 2>/dev/null"], $service->server, false); instant_remote_process(["docker network rm {$service->uuid} 2>/dev/null"], $service->server, false);
// TODO: make notification for databases
// $service->environment->project->team->notify(new StatusChanged($service));
} }
} }

View File

@@ -3,6 +3,7 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Models\Application; use App\Models\Application;
use App\Models\Server;
use App\Models\Service; use App\Models\Service;
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
use Illuminate\Console\Command; use Illuminate\Console\Command;
@@ -34,7 +35,7 @@ class ResourcesDelete extends Command
{ {
$resource = select( $resource = select(
'What resource do you want to delete?', 'What resource do you want to delete?',
['Application', 'Database', 'Service'], ['Application', 'Database', 'Service', 'Server'],
); );
if ($resource === 'Application') { if ($resource === 'Application') {
$this->deleteApplication(); $this->deleteApplication();
@@ -42,6 +43,30 @@ class ResourcesDelete extends Command
$this->deleteDatabase(); $this->deleteDatabase();
} elseif ($resource === 'Service') { } elseif ($resource === 'Service') {
$this->deleteService(); $this->deleteService();
} elseif ($resource === 'Server') {
$this->deleteServer();
}
}
private function deleteServer()
{
$servers = Server::all();
if ($servers->count() === 0) {
$this->error('There are no applications to delete.');
return;
}
$serversToDelete = multiselect(
label: 'What server do you want to delete?',
options: $servers->pluck('name', 'id')->sortKeys(),
);
foreach ($serversToDelete as $server) {
$toDelete = $servers->where('id', $server)->first();
$this->info($toDelete);
$confirmed = confirm("Are you sure you want to delete all selected resources?");
if (!$confirmed) {
break;
}
$toDelete->delete();
} }
} }
private function deleteApplication() private function deleteApplication()
@@ -53,14 +78,17 @@ class ResourcesDelete extends Command
} }
$applicationsToDelete = multiselect( $applicationsToDelete = multiselect(
'What application do you want to delete?', 'What application do you want to delete?',
$applications->pluck('name')->toArray(), $applications->pluck('name', 'id')->sortKeys(),
); );
$confirmed = confirm("Are you sure you want to delete all selected resources?");
if (!$confirmed) {
return;
}
foreach ($applicationsToDelete as $application) { foreach ($applicationsToDelete as $application) {
$toDelete = $applications->where('name', $application)->first(); ray($application);
$toDelete = $applications->where('id', $application)->first();
$this->info($toDelete);
$confirmed = confirm("Are you sure you want to delete all selected resources? ");
if (!$confirmed) {
break;
}
$toDelete->delete(); $toDelete->delete();
} }
} }
@@ -73,17 +101,18 @@ class ResourcesDelete extends Command
} }
$databasesToDelete = multiselect( $databasesToDelete = multiselect(
'What database do you want to delete?', 'What database do you want to delete?',
$databases->pluck('name')->toArray(), $databases->pluck('name', 'id')->sortKeys(),
); );
$confirmed = confirm("Are you sure you want to delete all selected resources?");
if (!$confirmed) {
return;
}
foreach ($databasesToDelete as $database) { foreach ($databasesToDelete as $database) {
$toDelete = $databases->where('name', $database)->first(); $toDelete = $databases->where('id', $database)->first();
$this->info($toDelete);
$confirmed = confirm("Are you sure you want to delete all selected resources?");
if (!$confirmed) {
return;
}
$toDelete->delete(); $toDelete->delete();
} }
} }
private function deleteService() private function deleteService()
{ {
@@ -94,14 +123,16 @@ class ResourcesDelete extends Command
} }
$servicesToDelete = multiselect( $servicesToDelete = multiselect(
'What service do you want to delete?', 'What service do you want to delete?',
$services->pluck('name')->toArray(), $services->pluck('name', 'id')->sortKeys(),
); );
$confirmed = confirm("Are you sure you want to delete all selected resources?");
if (!$confirmed) {
return;
}
foreach ($servicesToDelete as $service) { foreach ($servicesToDelete as $service) {
$toDelete = $services->where('name', $service)->first(); $toDelete = $services->where('id', $service)->first();
$this->info($toDelete);
$confirmed = confirm("Are you sure you want to delete all selected resources?");
if (!$confirmed) {
return;
}
$toDelete->delete(); $toDelete->delete();
} }
} }

View File

@@ -28,6 +28,7 @@ class Kernel extends ConsoleKernel
// $this->check_scheduled_backups($schedule); // $this->check_scheduled_backups($schedule);
$this->check_resources($schedule); $this->check_resources($schedule);
$this->cleanup_servers($schedule); $this->cleanup_servers($schedule);
$this->check_scheduled_backups($schedule);
} else { } else {
$schedule->command('horizon:snapshot')->everyFiveMinutes(); $schedule->command('horizon:snapshot')->everyFiveMinutes();
$schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer(); $schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer();

View File

@@ -19,7 +19,7 @@ class DatabaseController extends Controller
if (!$environment) { if (!$environment) {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
$database = $environment->databases->where('uuid', request()->route('database_uuid'))->first(); $database = $environment->databases()->where('uuid', request()->route('database_uuid'))->first();
if (!$database) { if (!$database) {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
@@ -37,7 +37,7 @@ class DatabaseController extends Controller
if (!$environment) { if (!$environment) {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
$database = $environment->databases->where('uuid', request()->route('database_uuid'))->first(); $database = $environment->databases()->where('uuid', request()->route('database_uuid'))->first();
if (!$database) { if (!$database) {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
@@ -64,10 +64,18 @@ class DatabaseController extends Controller
if (!$environment) { if (!$environment) {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
$database = $environment->databases->where('uuid', request()->route('database_uuid'))->first(); $database = $environment->databases()->where('uuid', request()->route('database_uuid'))->first();
if (!$database) { if (!$database) {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
// No backups for redis
if ($database->getMorphClass() === 'App\Models\StandaloneRedis') {
return redirect()->route('project.database.configuration', [
'project_uuid' => $project->uuid,
'environment_name' => $environment->name,
'database_uuid' => $database->uuid,
]);
}
return view('project.database.backups.all', [ return view('project.database.backups.all', [
'database' => $database, 'database' => $database,
's3s' => currentTeam()->s3s, 's3s' => currentTeam()->s3s,

View File

@@ -59,11 +59,15 @@ class ProjectController extends Controller
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
if (in_array($type, DATABASE_TYPES)) { if (in_array($type, DATABASE_TYPES)) {
$standalone_postgresql = create_standalone_postgresql($environment->id, $destination_uuid); if ($type->value() === "postgresql") {
$database = create_standalone_postgresql($environment->id, $destination_uuid);
} else if ($type->value() === 'redis') {
$database = create_standalone_redis($environment->id, $destination_uuid);
}
return redirect()->route('project.database.configuration', [ return redirect()->route('project.database.configuration', [
'project_uuid' => $project->uuid, 'project_uuid' => $project->uuid,
'environment_name' => $environment->name, 'environment_name' => $environment->name,
'database_uuid' => $standalone_postgresql->uuid, 'database_uuid' => $database->uuid,
]); ]);
} }
if ($type->startsWith('one-click-service-') && !is_null( (int)$server_id)) { if ($type->startsWith('one-click-service-') && !is_null( (int)$server_id)) {

View File

@@ -60,12 +60,16 @@ class DeploymentNavbar extends Component
$previous_logs[] = $new_log_entry; $previous_logs[] = $new_log_entry;
$this->application_deployment_queue->update([ $this->application_deployment_queue->update([
'logs' => json_encode($previous_logs, flags: JSON_THROW_ON_ERROR), 'logs' => json_encode($previous_logs, flags: JSON_THROW_ON_ERROR),
'current_process_id' => null,
'status' => ApplicationDeploymentStatus::CANCELLED_BY_USER->value,
]); ]);
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} finally {
$this->application_deployment_queue->update([
'current_process_id' => null,
'status' => ApplicationDeploymentStatus::CANCELLED_BY_USER->value,
]);
queue_next_deployment($this->application);
} }
} }
} }

View File

@@ -3,12 +3,9 @@
namespace App\Http\Livewire\Project\Application; namespace App\Http\Livewire\Project\Application;
use App\Models\Application; use App\Models\Application;
use App\Models\InstanceSettings;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Livewire\Component; use Livewire\Component;
use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml;
class General extends Component class General extends Component
{ {
@@ -23,6 +20,10 @@ class General extends Component
public ?string $git_commit_sha = null; public ?string $git_commit_sha = null;
public string $build_pack; public string $build_pack;
public $customLabels;
public bool $labelsChanged = false;
public bool $isConfigurationChanged = false;
public bool $is_static; public bool $is_static;
public bool $is_git_submodules_enabled; public bool $is_git_submodules_enabled;
public bool $is_git_lfs_enabled; public bool $is_git_lfs_enabled;
@@ -52,6 +53,7 @@ class General extends Component
'application.docker_registry_image_name' => 'nullable', 'application.docker_registry_image_name' => 'nullable',
'application.docker_registry_image_tag' => 'nullable', 'application.docker_registry_image_tag' => 'nullable',
'application.dockerfile_location' => 'nullable', 'application.dockerfile_location' => 'nullable',
'application.custom_labels' => 'nullable',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
'application.name' => 'name', 'application.name' => 'name',
@@ -73,9 +75,47 @@ class General extends Component
'application.docker_registry_image_name' => 'Docker registry image name', 'application.docker_registry_image_name' => 'Docker registry image name',
'application.docker_registry_image_tag' => 'Docker registry image tag', 'application.docker_registry_image_tag' => 'Docker registry image tag',
'application.dockerfile_location' => 'Dockerfile location', 'application.dockerfile_location' => 'Dockerfile location',
'application.custom_labels' => 'Custom labels',
]; ];
public function mount()
{
if (str($this->application->status)->startsWith('running') && is_null($this->application->config_hash)) {
$this->application->isConfigurationChanged(true);
}
$this->isConfigurationChanged = $this->application->isConfigurationChanged();
if (is_null(data_get($this->application, 'custom_labels'))) {
$this->customLabels = str(implode(",", generateLabelsApplication($this->application)))->replace(',', "\n");
} 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 updatedApplicationBuildPack()
{
if ($this->application->build_pack !== 'nixpacks') {
$this->application->settings->is_static = $this->is_static = false;
$this->application->settings->save();
}
$this->submit();
}
public function checkLabelUpdates()
{
if (md5($this->application->custom_labels) !== md5(implode(",", generateLabelsApplication($this->application)))) {
$this->labelsChanged = true;
} else {
$this->labelsChanged = false;
}
}
public function instantSave() public function instantSave()
{ {
// @TODO: find another way - if possible // @TODO: find another way - if possible
@@ -95,36 +135,41 @@ class General extends Component
$this->application->save(); $this->application->save();
$this->application->refresh(); $this->application->refresh();
$this->emit('success', 'Application settings updated!'); $this->emit('success', 'Application settings updated!');
$this->checkLabelUpdates();
$this->isConfigurationChanged = $this->application->isConfigurationChanged();
} }
public function getWildcardDomain() { public function getWildcardDomain()
{
$server = data_get($this->application, 'destination.server'); $server = data_get($this->application, 'destination.server');
if ($server) { if ($server) {
$fqdn = generateFqdn($server, $this->application->uuid); $fqdn = generateFqdn($server, $this->application->uuid);
ray($fqdn);
$this->application->fqdn = $fqdn; $this->application->fqdn = $fqdn;
$this->application->save(); $this->application->save();
$this->emit('success', 'Application settings updated!'); $this->emit('success', 'Application settings updated!');
} }
} }
public function mount() public function resetDefaultLabels($showToaster = true)
{ {
if (data_get($this->application,'settings')) { $this->customLabels = str(implode(",", generateLabelsApplication($this->application)))->replace(',', "\n");
$this->is_static = $this->application->settings->is_static; $this->submit($showToaster);
$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;
}
} }
public function submit() public function updatedApplicationFqdn()
{
$this->resetDefaultLabels(false);
$this->emit('success', 'Labels reseted to default!');
}
public function submit($showToaster = true)
{ {
try { try {
$this->validate(); $this->validate();
if (data_get($this->application, 'build_pack') === 'dockerimage') {
$this->validate([
'application.docker_registry_image_name' => 'required',
'application.docker_registry_image_tag' => 'required',
]);
}
if (data_get($this->application, 'fqdn')) { if (data_get($this->application, 'fqdn')) {
$domains = Str::of($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { $domains = Str::of($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
return Str::of($domain)->trim()->lower(); return Str::of($domain)->trim()->lower();
@@ -143,10 +188,17 @@ class General extends Component
if ($this->application->publish_directory && $this->application->publish_directory !== '/') { if ($this->application->publish_directory && $this->application->publish_directory !== '/') {
$this->application->publish_directory = rtrim($this->application->publish_directory, '/'); $this->application->publish_directory = rtrim($this->application->publish_directory, '/');
} }
if (gettype($this->customLabels) === 'string') {
$this->customLabels = str($this->customLabels)->replace(',', "\n");
}
$this->application->custom_labels = $this->customLabels->explode("\n")->implode(',');
$this->application->save(); $this->application->save();
$this->emit('success', 'Application settings updated!'); $showToaster && $this->emit('success', 'Application settings updated!');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} finally {
$this->checkLabelUpdates();
$this->isConfigurationChanged = $this->application->isConfigurationChanged();
} }
} }
} }

View File

@@ -2,6 +2,7 @@
namespace App\Http\Livewire\Project\Application; namespace App\Http\Livewire\Project\Application;
use App\Actions\Application\StopApplication;
use App\Jobs\ContainerStatusJob; use App\Jobs\ContainerStatusJob;
use App\Models\Application; use App\Models\Application;
use Livewire\Component; use Livewire\Component;
@@ -59,22 +60,9 @@ class Heading extends Component
public function stop() public function stop()
{ {
$containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id); StopApplication::run($this->application);
if ($containers->count() === 0) { $this->application->status = 'exited';
return; $this->application->save();
}
foreach ($containers as $container) {
$containerName = data_get($container, 'Names');
if ($containerName) {
instant_remote_process(
["docker rm -f {$containerName}"],
$this->application->destination->server
);
$this->application->status = 'exited';
$this->application->save();
// $this->application->environment->project->team->notify(new StatusChanged($this->application));
}
}
$this->application->refresh(); $this->application->refresh();
} }
} }

View File

@@ -17,6 +17,7 @@ class BackupEdit extends Component
'backup.number_of_backups_locally' => 'required|integer|min:1', 'backup.number_of_backups_locally' => 'required|integer|min:1',
'backup.save_s3' => 'required|boolean', 'backup.save_s3' => 'required|boolean',
'backup.s3_storage_id' => 'nullable|integer', 'backup.s3_storage_id' => 'nullable|integer',
'backup.databases_to_backup' => 'nullable',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
'backup.enabled' => 'Enabled', 'backup.enabled' => 'Enabled',
@@ -24,6 +25,7 @@ class BackupEdit extends Component
'backup.number_of_backups_locally' => 'Number of Backups Locally', 'backup.number_of_backups_locally' => 'Number of Backups Locally',
'backup.save_s3' => 'Save to S3', 'backup.save_s3' => 'Save to S3',
'backup.s3_storage_id' => 'S3 Storage', 'backup.s3_storage_id' => 'S3 Storage',
'backup.databases_to_backup' => 'Databases to Backup',
]; ];
protected $messages = [ protected $messages = [
'backup.s3_storage_id' => 'Select a S3 Storage', 'backup.s3_storage_id' => 'Select a S3 Storage',
@@ -37,7 +39,6 @@ class BackupEdit extends Component
} }
} }
public function delete() public function delete()
{ {
// TODO: Delete backup from server and add a confirmation modal // TODO: Delete backup from server and add a confirmation modal
@@ -49,6 +50,7 @@ class BackupEdit extends Component
{ {
try { try {
$this->custom_validate(); $this->custom_validate();
$this->backup->save(); $this->backup->save();
$this->backup->refresh(); $this->backup->refresh();
$this->emit('success', 'Backup updated successfully'); $this->emit('success', 'Backup updated successfully');
@@ -71,9 +73,11 @@ class BackupEdit extends Component
public function submit() public function submit()
{ {
ray($this->backup->s3_storage_id);
try { try {
$this->custom_validate(); $this->custom_validate();
if ($this->backup->databases_to_backup == '' || $this->backup->databases_to_backup === null) {
$this->backup->databases_to_backup = null;
}
$this->backup->save(); $this->backup->save();
$this->backup->refresh(); $this->backup->refresh();
$this->emit('success', 'Backup updated successfully'); $this->emit('success', 'Backup updated successfully');

View File

@@ -13,6 +13,6 @@ class BackupNow extends Component
dispatch(new DatabaseBackupJob( dispatch(new DatabaseBackupJob(
backup: $this->backup backup: $this->backup
)); ));
$this->emit('success', 'Backup queued. It will be available in a few minutes'); $this->emit('success', 'Backup queued. It will be available in a few minutes.');
} }
} }

View File

@@ -32,7 +32,7 @@ class CreateScheduledBackup extends Component
$this->emit('error', 'Invalid Cron / Human expression.'); $this->emit('error', 'Invalid Cron / Human expression.');
return; return;
} }
ScheduledDatabaseBackup::create([ $payload = [
'enabled' => true, 'enabled' => true,
'frequency' => $this->frequency, 'frequency' => $this->frequency,
'save_s3' => $this->save_s3, 'save_s3' => $this->save_s3,
@@ -40,7 +40,11 @@ class CreateScheduledBackup extends Component
'database_id' => $this->database->id, 'database_id' => $this->database->id,
'database_type' => $this->database->getMorphClass(), 'database_type' => $this->database->getMorphClass(),
'team_id' => currentTeam()->id, 'team_id' => currentTeam()->id,
]); ];
if ($this->database->type() === 'standalone-postgresql') {
$payload['databases_to_backup'] = $this->database->postgres_db;
}
ScheduledDatabaseBackup::create($payload);
$this->emit('refreshScheduledBackups'); $this->emit('refreshScheduledBackups');
} catch (\Throwable $e) { } catch (\Throwable $e) {
handleError($e, $this); handleError($e, $this);

View File

@@ -3,6 +3,8 @@
namespace App\Http\Livewire\Project\Database; namespace App\Http\Livewire\Project\Database;
use App\Actions\Database\StartPostgresql; use App\Actions\Database\StartPostgresql;
use App\Actions\Database\StartRedis;
use App\Actions\Database\StopDatabase;
use App\Jobs\ContainerStatusJob; use App\Jobs\ContainerStatusJob;
use Livewire\Component; use Livewire\Component;
@@ -35,24 +37,20 @@ class Heading extends Component
public function stop() public function stop()
{ {
instant_remote_process( StopDatabase::run($this->database);
["docker rm -f {$this->database->uuid}"],
$this->database->destination->server
);
if ($this->database->is_public) {
stopPostgresProxy($this->database);
$this->database->is_public = false;
}
$this->database->status = 'exited'; $this->database->status = 'exited';
$this->database->save(); $this->database->save();
$this->check_status(); $this->check_status();
// $this->database->environment->project->team->notify(new StatusChanged($this->database));
} }
public function start() public function start()
{ {
if ($this->database->type() === 'standalone-postgresql') { if ($this->database->type() === 'standalone-postgresql') {
$activity = resolve(StartPostgresql::class)($this->database->destination->server, $this->database); $activity = StartPostgresql::run($this->database->destination->server, $this->database);
$this->emit('newMonitorActivity', $activity->id);
}
if ($this->database->type() === 'standalone-redis') {
$activity = StartRedis::run($this->database->destination->server, $this->database);
$this->emit('newMonitorActivity', $activity->id); $this->emit('newMonitorActivity', $activity->id);
} }
} }

View File

@@ -2,6 +2,8 @@
namespace App\Http\Livewire\Project\Database\Postgresql; namespace App\Http\Livewire\Project\Database\Postgresql;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
use Exception; use Exception;
use Livewire\Component; use Livewire\Component;
@@ -67,10 +69,10 @@ class General extends Component
} }
if ($this->database->is_public) { if ($this->database->is_public) {
$this->emit('success', 'Starting TCP proxy...'); $this->emit('success', 'Starting TCP proxy...');
startPostgresProxy($this->database); StartDatabaseProxy::run($this->database);
$this->emit('success', 'Database is now publicly accessible.'); $this->emit('success', 'Database is now publicly accessible.');
} else { } else {
stopPostgresProxy($this->database); StopDatabaseProxy::run($this->database);
$this->emit('success', 'Database is no longer publicly accessible.'); $this->emit('success', 'Database is no longer publicly accessible.');
} }
$this->getDbUrl(); $this->getDbUrl();

View File

@@ -0,0 +1,94 @@
<?php
namespace App\Http\Livewire\Project\Database\Redis;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Models\StandaloneRedis;
use Exception;
use Livewire\Component;
class General extends Component
{
protected $listeners = ['refresh'];
public StandaloneRedis $database;
public string $db_url;
protected $rules = [
'database.name' => 'required',
'database.description' => 'nullable',
'database.redis_conf' => 'nullable',
'database.redis_password' => 'required',
'database.image' => 'required',
'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
];
protected $validationAttributes = [
'database.name' => 'Name',
'database.description' => 'Description',
'database.redis_conf' => 'Redis Configuration',
'database.redis_password' => 'Redis Password',
'database.image' => 'Image',
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
];
public function submit() {
try {
$this->validate();
if ($this->database->redis_conf === "") {
$this->database->redis_conf = null;
}
$this->database->save();
$this->emit('success', 'Database updated successfully.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function instantSave()
{
try {
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) {
$this->emit('success', 'Starting TCP proxy...');
StartDatabaseProxy::run($this->database);
$this->emit('success', 'Database is now publicly accessible.');
} else {
StopDatabaseProxy::run($this->database);
$this->emit('success', 'Database is no longer publicly accessible.');
}
$this->getDbUrl();
$this->database->save();
} catch(\Throwable $e) {
$this->database->is_public = !$this->database->is_public;
return handleError($e, $this);
}
}
public function refresh(): void
{
$this->database->refresh();
}
public function mount()
{
$this->getDbUrl();
}
public function getDbUrl() {
if ($this->database->is_public) {
$this->db_url = "redis://:{$this->database->redis_password}@{$this->database->destination->server->getIp}:{$this->database->public_port}/0";
} else {
$this->db_url = "redis://:{$this->database->redis_password}@{$this->database->uuid}:6379/0";
}
}
public function render()
{
return view('livewire.project.database.redis.general');
}
}

View File

@@ -98,7 +98,6 @@ class GithubPrivateRepositoryDeployKey extends Component
'name' => generate_random_name(), 'name' => generate_random_name(),
'git_repository' => $this->git_repository, 'git_repository' => $this->git_repository,
'git_branch' => $this->branch, 'git_branch' => $this->branch,
'git_full_url' => $this->git_repository,
'build_pack' => 'nixpacks', 'build_pack' => 'nixpacks',
'ports_exposes' => $this->port, 'ports_exposes' => $this->port,
'publish_directory' => $this->publish_directory, 'publish_directory' => $this->publish_directory,
@@ -112,7 +111,6 @@ class GithubPrivateRepositoryDeployKey extends Component
'name' => generate_random_name(), 'name' => generate_random_name(),
'git_repository' => $this->git_repository, 'git_repository' => $this->git_repository,
'git_branch' => $this->branch, 'git_branch' => $this->branch,
'git_full_url' => "git@$this->git_host:$this->git_repository.git",
'build_pack' => 'nixpacks', 'build_pack' => 'nixpacks',
'ports_exposes' => $this->port, 'ports_exposes' => $this->port,
'publish_directory' => $this->publish_directory, 'publish_directory' => $this->publish_directory,
@@ -158,6 +156,8 @@ class GithubPrivateRepositoryDeployKey extends Component
$this->git_host = $this->repository_url_parsed->getHost(); $this->git_host = $this->repository_url_parsed->getHost();
$this->git_repository = $this->repository_url_parsed->getSegment(1) . '/' . $this->repository_url_parsed->getSegment(2); $this->git_repository = $this->repository_url_parsed->getSegment(1) . '/' . $this->repository_url_parsed->getSegment(2);
$this->git_repository = Str::finish("git@$this->git_host:$this->git_repository", '.git'); $this->git_repository = Str::finish("git@$this->git_host:$this->git_repository", '.git');
} else {
$this->git_repository = $this->repository_url;
} }
$this->git_source = 'other'; $this->git_source = 'other';
} }

View File

@@ -72,10 +72,14 @@ class PublicGitRepository extends Component
public function load_branch() public function load_branch()
{ {
try { try {
$this->branch_found = false;
$this->validate([ $this->validate([
'repository_url' => 'required|url' 'repository_url' => 'required|url'
]); ]);
} catch (\Throwable $e) {
return handleError($e, $this);
}
try {
$this->branch_found = false;
$this->get_git_source(); $this->get_git_source();
$this->get_branch(); $this->get_branch();
$this->selected_branch = $this->git_branch; $this->selected_branch = $this->git_branch;

View File

@@ -13,13 +13,7 @@ class Index extends Component
public $databases; public $databases;
public array $parameters; public array $parameters;
public array $query; public array $query;
protected $rules = [ protected $listeners = ["refreshStacks","checkStatus"];
'service.docker_compose_raw' => 'required',
'service.docker_compose' => 'required',
'service.name' => 'required',
'service.description' => 'nullable',
];
protected $listeners = ["saveCompose"];
public function render() public function render()
{ {
return view('livewire.project.service.index'); return view('livewire.project.service.index');
@@ -32,17 +26,12 @@ class Index extends Component
$this->applications = $this->service->applications->sort(); $this->applications = $this->service->applications->sort();
$this->databases = $this->service->databases->sort(); $this->databases = $this->service->databases->sort();
} }
public function saveCompose($raw)
{
$this->service->docker_compose_raw = $raw;
$this->submit();
}
public function checkStatus() public function checkStatus()
{ {
dispatch_sync(new ContainerStatusJob($this->service->server)); dispatch_sync(new ContainerStatusJob($this->service->server));
$this->refreshStack(); $this->refreshStacks();
} }
public function refreshStack() public function refreshStacks()
{ {
$this->applications = $this->service->applications->sort(); $this->applications = $this->service->applications->sort();
$this->applications->each(function ($application) { $this->applications->each(function ($application) {
@@ -53,21 +42,4 @@ class Index extends Component
$database->refresh(); $database->refresh();
}); });
} }
public function submit()
{
try {
$this->validate();
$this->service->save();
$this->service->parse();
$this->service->refresh();
$this->service->saveComposeConfigs();
$this->refreshStack();
$this->emit('refreshEnvs');
$this->emit('success', 'Service saved successfully.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
} }

View File

@@ -6,8 +6,8 @@ use Livewire\Component;
class Modal extends Component class Modal extends Component
{ {
public function serviceStatusUpdated() { public function checkStatus() {
$this->emit('serviceStatusUpdated'); $this->emit('checkStatus');
} }
public function render() public function render()
{ {

View File

@@ -13,20 +13,15 @@ class Navbar extends Component
public Service $service; public Service $service;
public array $parameters; public array $parameters;
public array $query; public array $query;
protected $listeners = ['serviceStatusUpdated'];
public function render() public function render()
{ {
return view('livewire.project.service.navbar'); return view('livewire.project.service.navbar');
} }
public function serviceStatusUpdated()
public function checkStatus()
{ {
$this->check_status(); $this->emit('checkStatus');
}
public function check_status()
{
dispatch_sync(new ContainerStatusJob($this->service->server));
$this->service->refresh();
} }
public function deploy() public function deploy()
{ {
@@ -39,5 +34,6 @@ class Navbar extends Component
StopService::run($this->service); StopService::run($this->service);
$this->service->refresh(); $this->service->refresh();
$this->emit('success', 'Service stopped successfully.'); $this->emit('success', 'Service stopped successfully.');
$this->checkStatus();
} }
} }

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Http\Livewire\Project\Service;
use Livewire\Component;
class StackForm extends Component
{
public $service;
protected $listeners = ["saveCompose"];
protected $rules = [
'service.docker_compose_raw' => 'required',
'service.docker_compose' => 'required',
'service.name' => 'required',
'service.description' => 'nullable',
];
public function saveCompose($raw)
{
$this->service->docker_compose_raw = $raw;
$this->submit();
}
public function submit()
{
try {
$this->validate();
$this->service->save();
$this->service->parse();
$this->service->refresh();
$this->service->saveComposeConfigs();
$this->emit('refreshStacks');
$this->emit('refreshEnvs');
$this->emit('success', 'Service saved successfully.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.project.service.stack-form');
}
}

View File

@@ -2,7 +2,7 @@
namespace App\Http\Livewire\Project\Shared; namespace App\Http\Livewire\Project\Shared;
use App\Actions\Service\StopService; use App\Jobs\StopResourceJob;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
@@ -10,7 +10,7 @@ class Danger extends Component
{ {
public $resource; public $resource;
public array $parameters; public array $parameters;
public string|null $modalId = null; public ?string $modalId = null;
public function mount() public function mount()
{ {
@@ -20,22 +20,8 @@ class Danger extends Component
public function delete() public function delete()
{ {
// Should be queued
try { try {
if ($this->resource->type() === 'service') { StopResourceJob::dispatchSync($this->resource);
$server = $this->resource->server;
StopService::run($this->resource);
} else {
$destination = data_get($this->resource, 'destination');
if ($destination) {
$destination = $this->resource->destination->getMorphClass()::where('id', $this->resource->destination->id)->first();
$server = $destination->server;
}
if ($this->resource->destination->server->isFunctional()) {
instant_remote_process(["docker rm -f {$this->resource->uuid}"], $server);
}
}
$this->resource->delete();
return redirect()->route('project.resources', [ return redirect()->route('project.resources', [
'project_uuid' => $this->parameters['project_uuid'], 'project_uuid' => $this->parameters['project_uuid'],
'environment_name' => $this->parameters['environment_name'] 'environment_name' => $this->parameters['environment_name']

View File

@@ -75,6 +75,9 @@ class All extends Component
case 'standalone-postgresql': case 'standalone-postgresql':
$environment->standalone_postgresql_id = $this->resource->id; $environment->standalone_postgresql_id = $this->resource->id;
break; break;
case 'standalone-redis':
$environment->standalone_redis_id = $this->resource->id;
break;
case 'service': case 'service':
$environment->service_id = $this->resource->id; $environment->service_id = $this->resource->id;
break; break;

View File

@@ -6,12 +6,13 @@ use App\Models\Application;
use App\Models\Server; use App\Models\Server;
use App\Models\Service; use App\Models\Service;
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Livewire\Component; use Livewire\Component;
class Logs extends Component class Logs extends Component
{ {
public ?string $type = null; public ?string $type = null;
public Application|StandalonePostgresql|Service $resource; public Application|StandalonePostgresql|Service|StandaloneRedis $resource;
public Server $server; public Server $server;
public ?string $container = null; public ?string $container = null;
public $parameters; public $parameters;
@@ -33,7 +34,14 @@ class Logs extends Component
} }
} else if (data_get($this->parameters, 'database_uuid')) { } else if (data_get($this->parameters, 'database_uuid')) {
$this->type = 'database'; $this->type = 'database';
$this->resource = StandalonePostgresql::where('uuid', $this->parameters['database_uuid'])->firstOrFail(); $resource = StandalonePostgresql::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
$resource = StandaloneRedis::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
abort(404);
}
}
$this->resource = $resource;
$this->status = $this->resource->status; $this->status = $this->resource->status;
$this->server = $this->resource->destination->server; $this->server = $this->resource->destination->server;
$this->container = $this->resource->uuid; $this->container = $this->resource->uuid;

View File

@@ -30,11 +30,11 @@ class Form extends Component
'wildcard_domain' => 'nullable|url', 'wildcard_domain' => 'nullable|url',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
'server.name' => 'name', 'server.name' => 'Name',
'server.description' => 'description', 'server.description' => 'Description',
'server.ip' => 'ip', 'server.ip' => 'IP address',
'server.user' => 'user', 'server.user' => 'User',
'server.port' => 'port', 'server.port' => 'Port',
'server.settings.is_cloudflare_tunnel' => 'Cloudflare Tunnel', 'server.settings.is_cloudflare_tunnel' => 'Cloudflare Tunnel',
'server.settings.is_reachable' => 'is reachable', 'server.settings.is_reachable' => 'is reachable',
'server.settings.is_part_of_swarm' => 'is part of swarm' 'server.settings.is_part_of_swarm' => 'is part of swarm'
@@ -45,7 +45,8 @@ class Form extends Component
$this->wildcard_domain = $this->server->settings->wildcard_domain; $this->wildcard_domain = $this->server->settings->wildcard_domain;
$this->cleanup_after_percentage = $this->server->settings->cleanup_after_percentage; $this->cleanup_after_percentage = $this->server->settings->cleanup_after_percentage;
} }
public function serverRefresh() { public function serverRefresh()
{
$this->validateServer(); $this->validateServer();
} }
public function instantSave() public function instantSave()
@@ -61,7 +62,19 @@ class Form extends Component
$activity = InstallDocker::run($this->server); $activity = InstallDocker::run($this->server);
$this->emit('newMonitorActivity', $activity->id); $this->emit('newMonitorActivity', $activity->id);
} }
public function checkLocalhostConnection()
{
$uptime = $this->server->validateConnection();
if ($uptime) {
$this->emit('success', 'Server is reachable.');
$this->server->settings->is_reachable = true;
$this->server->settings->is_usable = true;
$this->server->settings->save();
} else {
$this->emit('error', 'Server is not reachable. Please check your connection and configuration.');
return;
}
}
public function validateServer($install = true) public function validateServer($install = true)
{ {
try { try {
@@ -69,7 +82,7 @@ class Form extends Component
if ($uptime) { if ($uptime) {
$install && $this->emit('success', 'Server is reachable.'); $install && $this->emit('success', 'Server is reachable.');
} else { } else {
$install &&$this->emit('error', 'Server is not reachable. Please check your connection and private key configuration.'); $install && $this->emit('error', 'Server is not reachable. Please check your connection and configuration.');
return; return;
} }
$dockerInstalled = $this->server->validateDockerEngine(); $dockerInstalled = $this->server->validateDockerEngine();
@@ -109,7 +122,14 @@ class Form extends Component
} }
public function submit() public function submit()
{ {
$this->validate(); if(isCloud() && !isDev()) {
$this->validate();
$this->validate([
'server.ip' => 'required|ip',
]);
} else {
$this->validate();
}
$uniqueIPs = Server::all()->reject(function (Server $server) { $uniqueIPs = Server::all()->reject(function (Server $server) {
return $server->id === $this->server->id; return $server->id === $this->server->id;
})->pluck('ip')->toArray(); })->pluck('ip')->toArray();
@@ -117,6 +137,7 @@ class Form extends Component
$this->emit('error', 'IP address is already in use by another team.'); $this->emit('error', 'IP address is already in use by another team.');
return; return;
} }
refresh_server_connection($this->server->privateKey);
$this->server->settings->wildcard_domain = $this->wildcard_domain; $this->server->settings->wildcard_domain = $this->wildcard_domain;
$this->server->settings->cleanup_after_percentage = $this->cleanup_after_percentage; $this->server->settings->cleanup_after_percentage = $this->cleanup_after_percentage;
$this->server->settings->save(); $this->server->settings->save();

View File

@@ -2,6 +2,7 @@
namespace App\Http\Livewire\Server\Proxy; namespace App\Http\Livewire\Server\Proxy;
use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy; use App\Actions\Proxy\StartProxy;
use App\Models\Server; use App\Models\Server;
use Livewire\Component; use Livewire\Component;
@@ -11,18 +12,40 @@ class Deploy extends Component
public Server $server; public Server $server;
public bool $traefikDashboardAvailable = false; public bool $traefikDashboardAvailable = false;
public ?string $currentRoute = null; public ?string $currentRoute = null;
protected $listeners = ['proxyStatusUpdated', 'traefikDashboardAvailable', 'serverRefresh' => 'proxyStatusUpdated']; public ?string $serverIp = null;
public function mount() { protected $listeners = ['proxyStatusUpdated', 'traefikDashboardAvailable', 'serverRefresh' => 'proxyStatusUpdated', "checkProxy", "startProxy"];
public function mount()
{
if ($this->server->id === 0) {
$this->serverIp = base_ip();
} else {
$this->serverIp = $this->server->ip;
}
$this->currentRoute = request()->route()->getName(); $this->currentRoute = request()->route()->getName();
} }
public function traefikDashboardAvailable(bool $data) { public function traefikDashboardAvailable(bool $data)
{
$this->traefikDashboardAvailable = $data; $this->traefikDashboardAvailable = $data;
} }
public function proxyStatusUpdated() public function proxyStatusUpdated()
{ {
$this->server->refresh(); $this->server->refresh();
} }
public function ip()
{
}
public function checkProxy()
{
try {
CheckProxy::run($this->server, true);
$this->emit('startProxyPolling');
$this->emit('proxyChecked');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function startProxy() public function startProxy()
{ {
try { try {

View File

@@ -11,8 +11,6 @@ class Modal extends Component
public function proxyStatusUpdated() public function proxyStatusUpdated()
{ {
$this->server->proxy->set('status', 'running');
$this->server->save();
$this->emit('proxyStatusUpdated'); $this->emit('proxyStatusUpdated');
} }
} }

View File

@@ -2,6 +2,7 @@
namespace App\Http\Livewire\Server\Proxy; namespace App\Http\Livewire\Server\Proxy;
use App\Actions\Proxy\CheckProxy;
use App\Jobs\ContainerStatusJob; use App\Jobs\ContainerStatusJob;
use App\Models\Server; use App\Models\Server;
use Livewire\Component; use Livewire\Component;
@@ -9,12 +10,42 @@ use Livewire\Component;
class Status extends Component class Status extends Component
{ {
public Server $server; public Server $server;
public bool $polling = false;
public int $numberOfPolls = 0;
protected $listeners = ['proxyStatusUpdated']; protected $listeners = ['proxyStatusUpdated', 'startProxyPolling'];
public function startProxyPolling()
{
$this->polling = true;
}
public function proxyStatusUpdated() public function proxyStatusUpdated()
{ {
$this->server->refresh(); $this->server->refresh();
} }
public function checkProxy(bool $notification = false)
{
try {
if ($this->polling) {
if ($this->numberOfPolls >= 10) {
$this->polling = false;
$this->numberOfPolls = 0;
$notification && $this->emit('error', 'Proxy is not running.');
return;
}
$this->numberOfPolls++;
}
CheckProxy::run($this->server, true);
$this->emit('proxyStatusUpdated');
if ($this->server->proxy->status === 'running') {
$this->polling = false;
$notification && $this->emit('success', 'Proxy is running.');
} else {
$notification && $this->emit('error', 'Proxy is not running.');
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function getProxyStatus() public function getProxyStatus()
{ {
try { try {
@@ -24,11 +55,4 @@ class Status extends Component
return handleError($e, $this); return handleError($e, $this);
} }
} }
public function getProxyStatusWithNoti()
{
if ($this->server->isFunctional()) {
$this->emit('success', 'Refreshed proxy status.');
$this->getProxyStatus();
}
}
} }

View File

@@ -78,10 +78,10 @@ class Backup extends Component
dispatch(new DatabaseBackupJob( dispatch(new DatabaseBackupJob(
backup: $this->backup backup: $this->backup
)); ));
$this->emit('success', 'Backup queued. It will be available in a few minutes'); $this->emit('success', 'Backup queued. It will be available in a few minutes.');
} }
public function submit() public function submit()
{ {
$this->emit('success', 'Backup updated successfully'); $this->emit('success', 'Backup updated successfully.');
} }
} }

View File

@@ -37,7 +37,7 @@ class Upgrade extends Component
return; return;
} }
$this->showProgress = true; $this->showProgress = true;
resolve(UpdateCoolify::class)(true); UpdateCoolify::run(true);
$this->emit('success', "Upgrading to {$this->latestVersion} version..."); $this->emit('success', "Upgrading to {$this->latestVersion} version...");
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);

View File

@@ -12,6 +12,9 @@ class DecideWhatToDoWithUser
public function handle(Request $request, Closure $next): Response public function handle(Request $request, Closure $next): Response
{ {
if (!auth()->user() || !isCloud() || isInstanceAdmin()) { if (!auth()->user() || !isCloud() || isInstanceAdmin()) {
if (!isCloud() && showBoarding() && !in_array($request->path(), allowedPathsForBoardingAccounts())) {
return redirect('boarding');
}
return $next($request); return $next($request);
} }
if (!auth()->user()->hasVerifiedEmail()) { if (!auth()->user()->hasVerifiedEmail()) {

View File

@@ -54,7 +54,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private ApplicationPreview|null $preview = null; private ApplicationPreview|null $preview = null;
private string $container_name; private string $container_name;
private string|null $currently_running_container_name = null; private ?string $currently_running_container_name = null;
private string $basedir; private string $basedir;
private string $workdir; private string $workdir;
private ?string $build_pack = null; private ?string $build_pack = null;
@@ -67,14 +67,18 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private $docker_compose; private $docker_compose;
private $docker_compose_base64; private $docker_compose_base64;
private string $dockerfile_location = '/Dockerfile'; private string $dockerfile_location = '/Dockerfile';
private ?string $addHosts = null;
private $log_model; private $log_model;
private Collection $saved_outputs; private Collection $saved_outputs;
private string $serverUser = 'root';
private string $serverUserHomeDir = '/root';
private string $dockerConfigFileExists = 'NOK';
public $tries = 1; public $tries = 1;
public function __construct(int $application_deployment_queue_id) public function __construct(int $application_deployment_queue_id)
{ {
ray()->clearScreen(); // ray()->clearScreen();
$this->application_deployment_queue = ApplicationDeploymentQueue::find($application_deployment_queue_id); $this->application_deployment_queue = ApplicationDeploymentQueue::find($application_deployment_queue_id);
$this->log_model = $this->application_deployment_queue; $this->log_model = $this->application_deployment_queue;
$this->application = Application::find($this->application_deployment_queue->application_id); $this->application = Application::find($this->application_deployment_queue->application_id);
@@ -92,13 +96,12 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} }
$this->destination = $this->application->destination->getMorphClass()::where('id', $this->application->destination->id)->first(); $this->destination = $this->application->destination->getMorphClass()::where('id', $this->application->destination->id)->first();
$this->server = $this->destination->server; $this->server = $this->destination->server;
$this->serverUser = $this->server->user;
$this->basedir = "/artifacts/{$this->deployment_uuid}"; $this->basedir = "/artifacts/{$this->deployment_uuid}";
$this->workdir = "{$this->basedir}" . rtrim($this->application->base_directory, '/'); $this->workdir = "{$this->basedir}" . rtrim($this->application->base_directory, '/');
$this->configuration_dir = application_configuration_dir() . "/{$this->application->uuid}"; $this->configuration_dir = application_configuration_dir() . "/{$this->application->uuid}";
$this->is_debug_enabled = $this->application->settings->is_debug_enabled; $this->is_debug_enabled = $this->application->settings->is_debug_enabled;
ray($this->basedir, $this->workdir);
$this->container_name = generateApplicationContainerName($this->application, $this->pull_request_id); $this->container_name = generateApplicationContainerName($this->application, $this->pull_request_id);
savePrivateKeyToFs($this->server); savePrivateKeyToFs($this->server);
$this->saved_outputs = collect(); $this->saved_outputs = collect();
@@ -138,24 +141,51 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->application_deployment_queue->update([ $this->application_deployment_queue->update([
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value, 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
]); ]);
// Generate custom host<->ip mapping
$allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server);
$allContainers = format_docker_command_output_to_json($allContainers);
$ips = collect([]);
if (count($allContainers) > 0) {
$allContainers = $allContainers[0];
foreach ($allContainers as $container) {
$containerName = data_get($container, 'Name');
if ($containerName === 'coolify-proxy') {
continue;
}
$containerIp = data_get($container, 'IPv4Address');
if ($containerName && $containerIp) {
$containerIp = str($containerIp)->before('/');
$ips->put($containerName, $containerIp->value());
}
}
}
$this->addHosts = $ips->map(function ($ip, $name) {
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);
try { try {
if ($this->application->dockerfile) { if ($this->application->dockerfile) {
$this->deploy_simple_dockerfile(); $this->deploy_simple_dockerfile();
} else if ($this->application->build_pack === 'dockerimage') { } else if ($this->application->build_pack === 'dockerimage') {
$this->deploy_dockerimage(); $this->deploy_dockerimage_buildpack();
} else if ($this->application->build_pack === 'dockerfile') { } else if ($this->application->build_pack === 'dockerfile') {
$this->deploy_dockerfile(); $this->deploy_dockerfile_buildpack();
} else { } else {
if ($this->pull_request_id !== 0) { if ($this->pull_request_id !== 0) {
$this->deploy_pull_request(); $this->deploy_pull_request();
} else { } else {
$this->deploy(); $this->deploy_nixpacks_buildpack();
} }
} }
if ($this->server->isProxyShouldRun()) { if ($this->server->isProxyShouldRun()) {
dispatch(new ContainerStatusJob($this->server)); dispatch(new ContainerStatusJob($this->server));
} }
$this->next(ApplicationDeploymentStatus::FINISHED->value); $this->next(ApplicationDeploymentStatus::FINISHED->value);
$this->application->isConfigurationChanged(true);
} catch (Exception $e) { } catch (Exception $e) {
ray($e); ray($e);
$this->fail($e); $this->fail($e);
@@ -184,41 +214,42 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} }
} }
private function deploy_docker_compose() // private function deploy_docker_compose()
{ // {
$dockercompose_base64 = base64_encode($this->application->dockercompose); // $dockercompose_base64 = base64_encode($this->application->dockercompose);
$this->execute_remote_command( // $this->execute_remote_command(
[ // [
"echo 'Starting deployment of {$this->application->name}.'" // "echo 'Starting deployment of {$this->application->name}.'"
], // ],
); // );
$this->prepare_builder_image(); // $this->prepare_builder_image();
$this->execute_remote_command( // $this->execute_remote_command(
[ // [
executeInDocker($this->deployment_uuid, "echo '$dockercompose_base64' | base64 -d > $this->workdir/docker-compose.yaml") // 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->application->git_repository}:build");
$this->production_image_name = Str::lower("{$this->application->uuid}:latest"); // $this->production_image_name = Str::lower("{$this->application->uuid}:latest");
$this->save_environment_variables(); // $this->save_environment_variables();
$containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id); // $containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id);
if ($containers->count() > 0) { // ray($containers);
foreach ($containers as $container) { // if ($containers->count() > 0) {
$containerName = data_get($container, 'Names'); // foreach ($containers as $container) {
if ($containerName) { // $containerName = data_get($container, 'Names');
instant_remote_process( // if ($containerName) {
["docker rm -f {$containerName}"], // instant_remote_process(
$this->application->destination->server // ["docker rm -f {$containerName}"],
); // $this->application->destination->server
} // );
} // }
} // }
// }
$this->execute_remote_command( // $this->execute_remote_command(
["echo -n 'Starting services (could take a while)...'"], // ["echo -n 'Starting services (could take a while)...'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d"), "hidden" => true], // [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d"), "hidden" => true],
); // );
} // }
private function save_environment_variables() private function save_environment_variables()
{ {
$envs = collect([]); $envs = collect([]);
@@ -256,7 +287,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->rolling_update(); $this->rolling_update();
} }
private function deploy_dockerimage() private function deploy_dockerimage_buildpack()
{ {
$this->dockerImage = $this->application->docker_registry_image_name; $this->dockerImage = $this->application->docker_registry_image_name;
$this->dockerImageTag = $this->application->docker_registry_image_tag; $this->dockerImageTag = $this->application->docker_registry_image_tag;
@@ -272,7 +303,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->rolling_update(); $this->rolling_update();
} }
private function deploy_dockerfile() private function deploy_dockerfile_buildpack()
{ {
if (data_get($this->application, 'dockerfile_location')) { if (data_get($this->application, 'dockerfile_location')) {
$this->dockerfile_location = $this->application->dockerfile_location; $this->dockerfile_location = $this->application->dockerfile_location;
@@ -297,10 +328,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->generate_compose_file(); $this->generate_compose_file();
$this->generate_build_env_variables(); $this->generate_build_env_variables();
$this->add_build_env_variables_to_dockerfile(); $this->add_build_env_variables_to_dockerfile();
// $this->build_image(); $this->build_image();
$this->rolling_update(); $this->rolling_update();
} }
private function deploy() private function deploy_nixpacks_buildpack()
{ {
$this->execute_remote_command( $this->execute_remote_command(
[ [
@@ -323,19 +354,22 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->execute_remote_command([ $this->execute_remote_command([
"docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found" "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()) { if (Str::of($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
$this->execute_remote_command([ $this->execute_remote_command([
"echo 'Docker Image found locally with the same Git Commit SHA {$this->application->uuid}:{$this->commit}. Build step skipped...'" "echo 'No configuration changed & Docker Image found locally with the same Git Commit SHA {$this->application->uuid}:{$this->commit}. Build step skipped.'",
]); ]);
$this->generate_compose_file(); $this->generate_compose_file();
$this->rolling_update(); $this->rolling_update();
return; return;
} }
if ($this->application->isConfigurationChanged()) {
$this->execute_remote_command([
"echo 'Configuration changed. Rebuilding image.'",
]);
}
} }
$this->cleanup_git(); $this->cleanup_git();
if ($this->application->build_pack === 'nixpacks') { $this->generate_nixpacks_confs();
$this->generate_nixpacks_confs();
}
$this->generate_compose_file(); $this->generate_compose_file();
$this->generate_build_env_variables(); $this->generate_build_env_variables();
$this->add_build_env_variables_to_dockerfile(); $this->add_build_env_variables_to_dockerfile();
@@ -429,7 +463,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->stop_running_container(); $this->stop_running_container();
$this->execute_remote_command( $this->execute_remote_command(
["echo -n 'Starting preview deployment.'"], ["echo -n 'Starting preview deployment.'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d >/dev/null"), "hidden" => true], [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d"), "hidden" => true],
); );
} }
@@ -437,7 +471,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
{ {
$pull = "--pull=always"; $pull = "--pull=always";
$helperImage = config('coolify.helper_image'); $helperImage = config('coolify.helper_image');
$runCommand = "docker run {$pull} -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}"; if ($this->dockerConfigFileExists === 'OK') {
$runCommand = "docker run {$pull} -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 {$pull} -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( $this->execute_remote_command(
[ [
@@ -511,8 +549,13 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} }
} }
if ($this->application->deploymentType() === 'deploy_key') { if ($this->application->deploymentType() === 'deploy_key') {
$port = 22;
preg_match('/(?<=:)\d+(?=\/)/', $this->application->git_repository, $matches);
if (count($matches) === 1) {
$port = $matches[0];
}
$private_key = base64_encode($this->application->private_key->private_key); $private_key = base64_encode($this->application->private_key->private_key);
$git_clone_command = "GIT_SSH_COMMAND=\"ssh -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$this->application->git_full_url} {$this->basedir}"; $git_clone_command = "GIT_SSH_COMMAND=\"ssh -p $port -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 = $this->set_git_import_settings($git_clone_command);
$commands = collect([ $commands = collect([
executeInDocker($this->deployment_uuid, "mkdir -p /root/.ssh"), executeInDocker($this->deployment_uuid, "mkdir -p /root/.ssh"),
@@ -612,6 +655,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$volume_names = $this->generate_local_persistent_volumes_only_volume_names(); $volume_names = $this->generate_local_persistent_volumes_only_volume_names();
$environment_variables = $this->generate_environment_variables($ports); $environment_variables = $this->generate_environment_variables($ports);
$labels = generateLabelsApplication($this->application, $this->preview);
if (data_get($this->application, 'custom_labels')) {
$labels = str($this->application->custom_labels)->explode(',')->toArray();
}
$docker_compose = [ $docker_compose = [
'version' => '3.8', 'version' => '3.8',
'services' => [ 'services' => [
@@ -620,7 +667,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
'container_name' => $this->container_name, 'container_name' => $this->container_name,
'restart' => RESTART_MODE, 'restart' => RESTART_MODE,
'environment' => $environment_variables, 'environment' => $environment_variables,
'labels' => generateLabelsApplication($this->application, $this->preview, $ports), 'labels' => $labels,
'expose' => $ports, 'expose' => $ports,
'networks' => [ 'networks' => [
$this->destination->network, $this->destination->network,
@@ -664,12 +711,12 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if (count($volume_names) > 0) { if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names; $docker_compose['volumes'] = $volume_names;
} }
if ($this->build_pack === 'dockerfile') { // if ($this->build_pack === 'dockerfile') {
$docker_compose['services'][$this->container_name]['build'] = [ // $docker_compose['services'][$this->container_name]['build'] = [
'context' => $this->workdir, // 'context' => $this->workdir,
'dockerfile' => $this->workdir . $this->dockerfile_location, // 'dockerfile' => $this->workdir . $this->dockerfile_location,
]; // ];
} // }
$this->docker_compose = Yaml::dump($docker_compose, 10); $this->docker_compose = Yaml::dump($docker_compose, 10);
$this->docker_compose_base64 = base64_encode($this->docker_compose); $this->docker_compose_base64 = base64_encode($this->docker_compose);
$this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d > {$this->workdir}/docker-compose.yml"), "hidden" => true]); $this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d > {$this->workdir}/docker-compose.yml"), "hidden" => true]);
@@ -762,7 +809,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($this->application->settings->is_static) { if ($this->application->settings->is_static) {
$this->execute_remote_command([ $this->execute_remote_command([
executeInDocker($this->deployment_uuid, "docker build --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"), "hidden" => true 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
]); ]);
$dockerfile = base64_encode("FROM {$this->application->static_image} $dockerfile = base64_encode("FROM {$this->application->static_image}
@@ -795,12 +842,12 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
executeInDocker($this->deployment_uuid, "echo '{$nginx_config}' | base64 -d > {$this->workdir}/nginx.conf") executeInDocker($this->deployment_uuid, "echo '{$nginx_config}' | base64 -d > {$this->workdir}/nginx.conf")
], ],
[ [
executeInDocker($this->deployment_uuid, "docker build --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-prod {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
] ]
); );
} else { } else {
$this->execute_remote_command([ $this->execute_remote_command([
executeInDocker($this->deployment_uuid, "docker build --network host -f {$this->workdir}/Dockerfile {$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}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
]); ]);
} }
} }
@@ -826,7 +873,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
{ {
$this->execute_remote_command( $this->execute_remote_command(
["echo -n 'Starting application (could take a while).'"], ["echo -n 'Starting application (could take a while).'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d >/dev/null"), "hidden" => true], [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true],
); );
} }
@@ -849,7 +896,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
private function add_build_env_variables_to_dockerfile() private function add_build_env_variables_to_dockerfile()
{ {
$this->execute_remote_command([ $this->execute_remote_command([
executeInDocker($this->deployment_uuid, "cat {$this->workdir}/Dockerfile"), "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")); $dockerfile = collect(Str::of($this->saved_outputs->get('dockerfile'))->trim()->explode("\n"));
@@ -858,7 +905,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
} }
$dockerfile_base64 = base64_encode($dockerfile->implode("\n")); $dockerfile_base64 = base64_encode($dockerfile->implode("\n"));
$this->execute_remote_command([ $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}"),
"hidden" => true "hidden" => true
]); ]);
} }

View File

@@ -2,6 +2,7 @@
namespace App\Jobs; namespace App\Jobs;
use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy; use App\Actions\Proxy\StartProxy;
use App\Models\ApplicationPreview; use App\Models\ApplicationPreview;
use App\Models\Server; use App\Models\Server;
@@ -27,11 +28,6 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
public $tries = 1; public $tries = 1;
public $timeout = 120; public $timeout = 120;
public function __construct(public Server $server)
{
$this->handle();
}
public function middleware(): array public function middleware(): array
{ {
return [(new WithoutOverlapping($this->server->uuid))->dontRelease()]; return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
@@ -41,10 +37,18 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
{ {
return $this->server->uuid; return $this->server->uuid;
} }
public function __construct(public Server $server)
{
if (isDev()) {
$this->handle();
}
}
public function handle() public function handle()
{ {
try { try {
// ray("checking server status for {$this->server->name}"); // ray("checking server status for {$this->server->id}");
// ray()->clearAll(); // ray()->clearAll();
$serverUptimeCheckNumber = $this->server->unreachable_count; $serverUptimeCheckNumber = $this->server->unreachable_count;
$serverUptimeCheckNumberMax = 3; $serverUptimeCheckNumberMax = 3;
@@ -53,12 +57,15 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
if ($serverUptimeCheckNumber >= $serverUptimeCheckNumberMax) { if ($serverUptimeCheckNumber >= $serverUptimeCheckNumberMax) {
if ($this->server->unreachable_email_sent === false) { if ($this->server->unreachable_email_sent === false) {
ray('Server unreachable, sending notification...'); ray('Server unreachable, sending notification...');
// $this->server->team->notify(new Unreachable($this->server)); $this->server->team->notify(new Unreachable($this->server));
$this->server->update(['unreachable_email_sent' => true]); $this->server->update(['unreachable_email_sent' => true]);
} }
$this->server->settings()->update([ $this->server->settings()->update([
'is_reachable' => false, 'is_reachable' => false,
]); ]);
$this->server->update([
'unreachable_count' => 0,
]);
return; return;
} }
$result = $this->server->validateConnection(); $result = $this->server->validateConnection();
@@ -82,7 +89,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
if (data_get($this->server, 'unreachable_email_sent') === true) { if (data_get($this->server, 'unreachable_email_sent') === true) {
ray('Server is reachable again, sending notification...'); ray('Server is reachable again, sending notification...');
// $this->server->team->notify(new Revived($this->server)); $this->server->team->notify(new Revived($this->server));
$this->server->update(['unreachable_email_sent' => false]); $this->server->update(['unreachable_email_sent' => false]);
} }
if ( if (
@@ -111,11 +118,18 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
return data_get($value, 'Name') === '/coolify-proxy'; return data_get($value, 'Name') === '/coolify-proxy';
})->first(); })->first();
if (!$foundProxyContainer) { if (!$foundProxyContainer) {
ray('Proxy not found, starting it...'); try {
if ($this->server->isProxyShouldRun()) { $shouldStart = CheckProxy::run($this->server);
StartProxy::run($this->server, false); if ($shouldStart) {
$this->server->team->notify(new ContainerRestarted('coolify-proxy', $this->server)); 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 { } else {
$this->server->proxy->status = data_get($foundProxyContainer, 'State.Status'); $this->server->proxy->status = data_get($foundProxyContainer, 'State.Status');
$this->server->save(); $this->server->save();

View File

@@ -66,50 +66,77 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
ray('database not running'); ray('database not running');
return; return;
} }
$databaseType = $this->database->type();
$databasesToBackup = data_get($this->backup, 'databases_to_backup');
if (is_null($databasesToBackup)) {
if ($databaseType === 'standalone-postgresql') {
$databasesToBackup = [$this->database->postgres_db];
} else {
return;
}
} else {
$databasesToBackup = explode(',', $databasesToBackup);
$databasesToBackup = array_map('trim', $databasesToBackup);
}
$this->container_name = $this->database->uuid; $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->container_name;
if ($this->database->name === 'coolify-db') { if ($this->database->name === 'coolify-db') {
$databasesToBackup = ['coolify'];
$this->container_name = "coolify-db"; $this->container_name = "coolify-db";
$ip = Str::slug($this->server->ip); $ip = Str::slug($this->server->ip);
$this->backup_dir = backup_dir() . "/coolify" . "/coolify-db-$ip"; $this->backup_dir = backup_dir() . "/coolify" . "/coolify-db-$ip";
} }
$this->backup_file = "/pg-backup-customformat-" . Carbon::now()->timestamp . ".backup"; foreach ($databasesToBackup as $database) {
$this->backup_location = $this->backup_dir . $this->backup_file; $size = 0;
ray('Backing up ' . $database);
$this->backup_log = ScheduledDatabaseBackupExecution::create([ try {
'filename' => $this->backup_location, $this->backup_file = "/pg-dump-$database-" . Carbon::now()->timestamp . ".dmp";
'scheduled_database_backup_id' => $this->backup->id, $this->backup_location = $this->backup_dir . $this->backup_file;
]); $this->backup_log = ScheduledDatabaseBackupExecution::create([
if ($this->database->type() === 'standalone-postgresql') { 'database_name' => $database,
$this->backup_standalone_postgresql(); 'filename' => $this->backup_location,
'scheduled_database_backup_id' => $this->backup->id,
]);
if ($databaseType === 'standalone-postgresql') {
$this->backup_standalone_postgresql($database);
}
$size = $this->calculate_size();
$this->remove_old_backups();
if ($this->backup->save_s3) {
$this->upload_to_s3();
}
$this->team->notify(new BackupSuccess($this->backup, $this->database));
$this->backup_log->update([
'status' => 'success',
'message' => $this->backup_output,
'size' => $size,
]);
} catch (\Throwable $e) {
$this->backup_log->update([
'status' => 'failed',
'message' => $this->backup_output,
'size' => $size,
'filename' => null
]);
send_internal_notification('DatabaseBackupJob failed with: ' . $e->getMessage());
$this->team->notify(new BackupFailed($this->backup, $this->database, $this->backup_output));
throw $e;
}
} }
$this->calculate_size();
$this->remove_old_backups();
if ($this->backup->save_s3) {
$this->upload_to_s3();
}
$this->save_backup_logs();
$this->team->notify(new BackupSuccess($this->backup, $this->database));
$this->backup_status = 'success';
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->backup_status = 'failed';
send_internal_notification('DatabaseBackupJob failed with: ' . $e->getMessage()); send_internal_notification('DatabaseBackupJob failed with: ' . $e->getMessage());
$this->team->notify(new BackupFailed($this->backup, $this->database, $this->backup_output));
throw $e; throw $e;
} finally {
$this->backup_log->update([
'status' => $this->backup_status,
]);
} }
} }
private function backup_standalone_postgresql(): void private function backup_standalone_postgresql(string $database): void
{ {
try { try {
ray($this->backup_dir); ray($this->backup_dir);
$commands[] = "mkdir -p " . $this->backup_dir; $commands[] = "mkdir -p " . $this->backup_dir;
$commands[] = "docker exec $this->container_name pg_dump -Fc -U {$this->database->postgres_user} > $this->backup_location"; $commands[] = "docker exec $this->container_name pg_dump --format=custom --no-acl --no-owner --username {$this->database->postgres_user} $database > $this->backup_location";
$this->backup_output = instant_remote_process($commands, $this->server); $this->backup_output = instant_remote_process($commands, $this->server);
$this->backup_output = trim($this->backup_output); $this->backup_output = trim($this->backup_output);
if ($this->backup_output === '') { if ($this->backup_output === '') {
@@ -119,6 +146,7 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->add_to_backup_output($e->getMessage()); $this->add_to_backup_output($e->getMessage());
ray('Backup failed for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location . '\n\nError:' . $e->getMessage()); ray('Backup failed for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location . '\n\nError:' . $e->getMessage());
throw $e;
} }
} }
@@ -131,9 +159,9 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
} }
} }
private function calculate_size(): void private function calculate_size()
{ {
$this->size = instant_remote_process(["du -b $this->backup_location | cut -f1"], $this->server); return instant_remote_process(["du -b $this->backup_location | cut -f1"], $this->server, false);
} }
private function remove_old_backups(): void private function remove_old_backups(): void
@@ -180,13 +208,4 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
instant_remote_process([$command], $this->server); instant_remote_process([$command], $this->server);
} }
} }
private function save_backup_logs(): void
{
$this->backup_log->update([
'status' => $this->backup_status,
'message' => $this->backup_output,
'size' => $this->size,
]);
}
} }

View File

@@ -47,20 +47,7 @@ class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted
if (!$this->server->isFunctional()) { if (!$this->server->isFunctional()) {
return; return;
} }
if (isDev()) { $this->dockerRootFilesystem = "/";
$this->dockerRootFilesystem = "/";
} else {
$this->dockerRootFilesystem = instant_remote_process(
[
"stat --printf=%m $(docker info --format '{{json .DockerRootDir}}'' |sed 's/\"//g')"
],
$this->server,
false
);
}
if (!$this->dockerRootFilesystem) {
return;
}
$this->usageBefore = $this->getFilesystemUsage(); $this->usageBefore = $this->getFilesystemUsage();
if ($this->usageBefore >= $this->server->settings->cleanup_after_percentage) { if ($this->usageBefore >= $this->server->settings->cleanup_after_percentage) {
ray('Cleaning up ' . $this->server->name)->color('orange'); ray('Cleaning up ' . $this->server->name)->color('orange');

View File

@@ -23,6 +23,6 @@ class InstanceAutoUpdateJob implements ShouldQueue, ShouldBeUnique, ShouldBeEncr
public function handle(): void public function handle(): void
{ {
resolve(UpdateCoolify::class)($this->force); UpdateCoolify::run($this->force);
} }
} }

View File

@@ -0,0 +1,55 @@
<?php
namespace App\Jobs;
use App\Actions\Application\StopApplication;
use App\Actions\Database\StopDatabase;
use App\Actions\Service\StopService;
use App\Models\Application;
use App\Models\Service;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
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 StopResourceJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public Application|Service|StandalonePostgresql|StandaloneRedis $resource)
{
}
public function handle()
{
try {
$server = $this->resource->destination->server;
if (!$server->isFunctional()) {
return 'Server is not functional';
}
switch ($this->resource->type()) {
case 'application':
StopApplication::run($this->resource);
break;
case 'standalone-postgresql':
StopDatabase::run($this->resource);
break;
case 'standalone-redis':
StopDatabase::run($this->resource);
break;
case 'service':
StopService::run($this->resource);
break;
}
} catch (\Throwable $e) {
send_internal_notification('ContainerStoppingJob failed with: ' . $e->getMessage());
throw $e;
} finally {
$this->resource->delete();
}
}
}

View File

@@ -32,16 +32,8 @@ class Application extends BaseModel
]); ]);
}); });
static::deleting(function ($application) { static::deleting(function ($application) {
// Stop Container
if ($application->destination->server->isFunctional()) {
instant_remote_process(
["docker rm -f {$application->uuid}"],
$application->destination->server,
false
);
}
$application->settings()->delete(); $application->settings()->delete();
$storages = $application->persistentStorages()->get(); $storages = $application->persistentStorages()->get();
foreach ($storages as $storage) { foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $application->destination->server, false); instant_remote_process(["docker volume rm -f $storage->name"], $application->destination->server, false);
} }
@@ -285,4 +277,31 @@ class Application extends BaseModel
} }
return false; return 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;
if ($this->pull_request_id === 0) {
$newConfigHash .= json_encode($this->environment_variables->all());
} else {
$newConfigHash .= json_encode($this->environment_variables_preview->all());
}
$newConfigHash = md5($newConfigHash);
$oldConfigHash = data_get($this, 'config_hash');
if ($oldConfigHash === null) {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
if ($oldConfigHash === $newConfigHash) {
return false;
} else {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
}
} }

View File

@@ -14,7 +14,7 @@ class Environment extends Model
public function can_delete_environment() public function can_delete_environment()
{ {
return $this->applications()->count() == 0 && $this->postgresqls()->count() == 0 && $this->services()->count() == 0; return $this->applications()->count() == 0 && $this->redis()->count() == 0 && $this->postgresqls()->count() == 0 && $this->services()->count() == 0;
} }
public function applications() public function applications()
@@ -26,10 +26,16 @@ class Environment extends Model
{ {
return $this->hasMany(StandalonePostgresql::class); return $this->hasMany(StandalonePostgresql::class);
} }
public function redis()
{
return $this->hasMany(StandaloneRedis::class);
}
public function databases() public function databases()
{ {
return $this->postgresqls(); $postgresqls = $this->postgresqls;
$redis = $this->redis;
return $postgresqls->concat($redis);
} }
public function project() public function project()

View File

@@ -52,4 +52,8 @@ class Project extends BaseModel
{ {
return $this->hasManyThrough(StandalonePostgresql::class, Environment::class); return $this->hasManyThrough(StandalonePostgresql::class, Environment::class);
} }
public function redis()
{
return $this->hasManyThrough(StandaloneRedis::class, Environment::class);
}
} }

View File

@@ -93,8 +93,11 @@ class Server extends BaseModel
public function proxyType() public function proxyType()
{ {
$type = $this->proxy->get('type'); $proxyType = $this->proxy->get('type');
if (is_null($type)) { if ($proxyType === ProxyTypes::NONE->value) {
return $proxyType;
}
if (is_null($proxyType)) {
$this->proxy->type = ProxyTypes::TRAEFIK_V2->value; $this->proxy->type = ProxyTypes::TRAEFIK_V2->value;
$this->proxy->status = ProxyStatus::EXITED->value; $this->proxy->status = ProxyStatus::EXITED->value;
$this->save(); $this->save();
@@ -120,7 +123,8 @@ class Server extends BaseModel
{ {
return $this->destinations()->map(function ($standaloneDocker) { return $this->destinations()->map(function ($standaloneDocker) {
$postgresqls = $standaloneDocker->postgresqls; $postgresqls = $standaloneDocker->postgresqls;
return $postgresqls?->concat([]) ?? collect([]); $redis = $standaloneDocker->redis;
return $postgresqls->concat($redis);
})->flatten(); })->flatten();
} }
public function applications() public function applications()
@@ -189,23 +193,24 @@ class Server extends BaseModel
} }
public function isProxyShouldRun() public function isProxyShouldRun()
{ {
$shouldRun = false;
if ($this->proxyType() === ProxyTypes::NONE->value) { if ($this->proxyType() === ProxyTypes::NONE->value) {
return false; return false;
} }
foreach ($this->applications() as $application) { // foreach ($this->applications() as $application) {
if (data_get($application, 'fqdn')) { // if (data_get($application, 'fqdn')) {
$shouldRun = true; // $shouldRun = true;
break; // break;
} // }
} // }
if ($this->id === 0) { // ray($this->services()->get());
$settings = InstanceSettings::get();
if (data_get($settings, 'fqdn')) { // if ($this->id === 0) {
$shouldRun = true; // $settings = InstanceSettings::get();
} // if (data_get($settings, 'fqdn')) {
} // $shouldRun = true;
return $shouldRun; // }
// }
return true;
} }
public function isFunctional() public function isFunctional()
{ {

View File

@@ -19,7 +19,6 @@ class Service extends BaseModel
static::deleting(function ($service) { static::deleting(function ($service) {
$storagesToDelete = collect([]); $storagesToDelete = collect([]);
foreach ($service->applications()->get() as $application) { foreach ($service->applications()->get() as $application) {
instant_remote_process(["docker rm -f {$application->name}-{$service->uuid}"], $service->server, false);
$storages = $application->persistentStorages()->get(); $storages = $application->persistentStorages()->get();
foreach ($storages as $storage) { foreach ($storages as $storage) {
$storagesToDelete->push($storage); $storagesToDelete->push($storage);
@@ -27,7 +26,6 @@ class Service extends BaseModel
$application->persistentStorages()->delete(); $application->persistentStorages()->delete();
} }
foreach ($service->databases()->get() as $database) { foreach ($service->databases()->get() as $database) {
instant_remote_process(["docker rm -f {$database->name}-{$service->uuid}"], $service->server, false);
$storages = $database->persistentStorages()->get(); $storages = $database->persistentStorages()->get();
foreach ($storages as $storage) { foreach ($storages as $storage) {
$storagesToDelete->push($storage); $storagesToDelete->push($storage);
@@ -384,7 +382,7 @@ class Service extends BaseModel
$value = Str::of($variable); $value = Str::of($variable);
} }
if ($key->startsWith('SERVICE_FQDN')) { if ($key->startsWith('SERVICE_FQDN')) {
if ($isNew) { if ($isNew || $savedService->fqdn === null) {
$name = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower(); $name = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower();
$fqdn = generateFqdn($this->server, "{$name->value()}-{$this->uuid}"); $fqdn = generateFqdn($this->server, "{$name->value()}-{$this->uuid}");
if (substr_count($key->value(), '_') === 3) { if (substr_count($key->value(), '_') === 3) {
@@ -540,7 +538,7 @@ class Service extends BaseModel
$serviceLabels = $serviceLabels->merge($defaultLabels); $serviceLabels = $serviceLabels->merge($defaultLabels);
if (!$isDatabase && $fqdns->count() > 0) { if (!$isDatabase && $fqdns->count() > 0) {
if ($fqdns) { if ($fqdns) {
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($fqdns, true)); $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($this->uuid, $fqdns, true));
} }
} }
data_set($service, 'labels', $serviceLabels->toArray()); data_set($service, 'labels', $serviceLabels->toArray());
@@ -570,7 +568,7 @@ class Service extends BaseModel
'networks' => $topLevelNetworks->toArray(), 'networks' => $topLevelNetworks->toArray(),
]; ];
$this->docker_compose_raw = Yaml::dump($yaml, 10, 2); $this->docker_compose_raw = Yaml::dump($yaml, 10, 2);
$this->docker_compose = Yaml::dump($finalServices, 10, 2); $this->docker_compose = Yaml::dump($finalServices, 10, 2);
$this->save(); $this->save();
$this->saveComposeConfigs(); $this->saveComposeConfigs();
return collect([]); return collect([]);

View File

@@ -15,6 +15,10 @@ class StandaloneDocker extends BaseModel
{ {
return $this->morphMany(StandalonePostgresql::class, 'destination'); return $this->morphMany(StandalonePostgresql::class, 'destination');
} }
public function redis()
{
return $this->morphMany(StandaloneRedis::class, 'destination');
}
public function server() public function server()
{ {

View File

@@ -29,21 +29,13 @@ class StandalonePostgresql extends BaseModel
]); ]);
}); });
static::deleting(function ($database) { static::deleting(function ($database) {
// Stop Container $storages = $database->persistentStorages()->get();
instant_remote_process( foreach ($storages as $storage) {
["docker rm -f {$database->uuid}"], instant_remote_process(["docker volume rm -f $storage->name"], $database->destination->server, false);
$database->destination->server,
false
);
// Stop TCP Proxy
if ($database->is_public) {
instant_remote_process(["docker rm -f {$database->uuid}-proxy"], $database->destination->server, false);
} }
$database->scheduledBackups()->delete(); $database->scheduledBackups()->delete();
$database->persistentStorages()->delete(); $database->persistentStorages()->delete();
$database->environment_variables()->delete(); $database->environment_variables()->delete();
// Remove Volume
instant_remote_process(['docker volume rm postgres-data-' . $database->uuid], $database->destination->server, false);
}); });
} }

View File

@@ -0,0 +1,95 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
class StandaloneRedis extends BaseModel
{
use HasFactory;
protected $guarded = [];
protected static function booted()
{
static::created(function ($database) {
LocalPersistentVolume::create([
'name' => 'redis-data-' . $database->uuid,
'mount_path' => '/data',
'host_path' => null,
'resource_id' => $database->id,
'resource_type' => $database->getMorphClass(),
'is_readonly' => true
]);
});
static::deleting(function ($database) {
$database->scheduledBackups()->delete();
$storages = $database->persistentStorages()->get();
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $database->destination->server, false);
}
$database->persistentStorages()->delete();
$database->environment_variables()->delete();
});
}
public function portsMappings(): Attribute
{
return Attribute::make(
set: fn ($value) => $value === "" ? null : $value,
);
}
// Normal Deployments
public function portsMappingsArray(): Attribute
{
return Attribute::make(
get: fn () => is_null($this->ports_mappings)
? []
: explode(',', $this->ports_mappings),
);
}
public function type(): string
{
return 'standalone-redis';
}
public function environment()
{
return $this->belongsTo(Environment::class);
}
public function fileStorages()
{
return $this->morphMany(LocalFileVolume::class, 'resource');
}
public function destination()
{
return $this->morphTo();
}
public function environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class);
}
public function runtime_environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class);
}
public function persistentStorages()
{
return $this->morphMany(LocalPersistentVolume::class, 'resource');
}
public function scheduledBackups()
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
}

View File

@@ -72,7 +72,7 @@ class User extends Authenticatable implements SendsEmail
$mail->view('emails.email-verification', [ $mail->view('emails.email-verification', [
'url' => $url, 'url' => $url,
]); ]);
$mail->subject('Coolify Cloud: Verify your email.'); $mail->subject('Coolify: Verify your email.');
send_user_an_email($mail, $this->email); send_user_an_email($mail, $this->email);
} }
public function sendPasswordResetNotification($token): void public function sendPasswordResetNotification($token): void

View File

@@ -15,25 +15,23 @@ class StatusChanged extends Notification implements ShouldQueue
public $tries = 1; public $tries = 1;
public Application $application; public string $resource_name;
public string $application_name;
public string $project_uuid; public string $project_uuid;
public string $environment_name; public string $environment_name;
public ?string $application_url = null; public ?string $resource_url = null;
public ?string $fqdn; public ?string $fqdn;
public function __construct($application) public function __construct(public Application $resource)
{ {
$this->application = $application; $this->resource_name = data_get($resource, 'name');
$this->application_name = data_get($application, 'name'); $this->project_uuid = data_get($resource, 'environment.project.uuid');
$this->project_uuid = data_get($application, 'environment.project.uuid'); $this->environment_name = data_get($resource, 'environment.name');
$this->environment_name = data_get($application, 'environment.name'); $this->fqdn = data_get($resource, 'fqdn', null);
$this->fqdn = data_get($application, 'fqdn', null);
if (Str::of($this->fqdn)->explode(',')->count() > 1) { if (Str::of($this->fqdn)->explode(',')->count() > 1) {
$this->fqdn = Str::of($this->fqdn)->explode(',')->first(); $this->fqdn = Str::of($this->fqdn)->explode(',')->first();
} }
$this->application_url = base_url() . "/project/{$this->project_uuid}/{$this->environment_name}/application/{$this->application->uuid}"; $this->resource_url = base_url() . "/project/{$this->project_uuid}/{$this->environment_name}/application/{$this->resource->uuid}";
} }
public function via(object $notifiable): array public function via(object $notifiable): array
@@ -45,32 +43,32 @@ class StatusChanged extends Notification implements ShouldQueue
{ {
$mail = new MailMessage(); $mail = new MailMessage();
$fqdn = $this->fqdn; $fqdn = $this->fqdn;
$mail->subject("Coolify: {$this->application_name} has been stopped"); $mail->subject("Coolify: {$this->resource_name} has been stopped");
$mail->view('emails.application-status-changes', [ $mail->view('emails.application-status-changes', [
'name' => $this->application_name, 'name' => $this->resource_name,
'fqdn' => $fqdn, 'fqdn' => $fqdn,
'application_url' => $this->application_url, 'resource_url' => $this->resource_url,
]); ]);
return $mail; return $mail;
} }
public function toDiscord(): string public function toDiscord(): string
{ {
$message = 'Coolify: ' . $this->application_name . ' has been stopped. $message = 'Coolify: ' . $this->resource_name . ' has been stopped.
'; ';
$message .= '[Open Application in Coolify](' . $this->application_url . ')'; $message .= '[Open Application in Coolify](' . $this->resource_url . ')';
return $message; return $message;
} }
public function toTelegram(): array public function toTelegram(): array
{ {
$message = 'Coolify: ' . $this->application_name . ' has been stopped.'; $message = 'Coolify: ' . $this->resource_name . ' has been stopped.';
return [ return [
"message" => $message, "message" => $message,
"buttons" => [ "buttons" => [
[ [
"text" => "Open Application in Coolify", "text" => "Open Application in Coolify",
"url" => $this->application_url "url" => $this->resource_url
] ]
], ],
]; ];

View File

@@ -1,6 +1,6 @@
<?php <?php
const DATABASE_TYPES = ['postgresql']; const DATABASE_TYPES = ['postgresql','redis'];
const VALID_CRON_STRINGS = [ const VALID_CRON_STRINGS = [
'every_minute' => '* * * * *', 'every_minute' => '* * * * *',
'hourly' => '0 * * * *', 'hourly' => '0 * * * *',

View File

@@ -3,6 +3,7 @@
use App\Models\Server; use App\Models\Server;
use App\Models\StandaloneDocker; use App\Models\StandaloneDocker;
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
function generate_database_name(string $type): string function generate_database_name(string $type): string
@@ -27,6 +28,21 @@ function create_standalone_postgresql($environment_id, $destination_uuid): Stand
]); ]);
} }
function create_standalone_redis($environment_id, $destination_uuid): StandaloneRedis
{
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
if (!$destination) {
throw new Exception('Destination not found');
}
return StandaloneRedis::create([
'name' => generate_database_name('redis'),
'redis_password' => \Illuminate\Support\Str::password(symbols: false),
'environment_id' => $environment_id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
]);
}
/** /**
* Delete file locally on the filesystem. * Delete file locally on the filesystem.
* @param string $filename * @param string $filename

View File

@@ -147,12 +147,11 @@ function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'applica
} }
return $labels; return $labels;
} }
function fqdnLabelsForTraefik(Collection $domains, bool $is_force_https_enabled, $onlyPort = null) function fqdnLabelsForTraefik($uuid, Collection $domains, bool $is_force_https_enabled, $onlyPort = null)
{ {
$labels = collect([]); $labels = collect([]);
$labels->push('traefik.enable=true'); $labels->push('traefik.enable=true');
foreach ($domains as $domain) { foreach ($domains as $domain) {
$uuid = (string)new Cuid2(7);
$url = Url::fromString($domain); $url = Url::fromString($domain);
$host = $url->getHost(); $host = $url->getHost();
$path = $url->getPath(); $path = $url->getPath();
@@ -205,20 +204,21 @@ function fqdnLabelsForTraefik(Collection $domains, bool $is_force_https_enabled,
return $labels; return $labels;
} }
function generateLabelsApplication(Application $application, ?ApplicationPreview $preview = null, $ports): array function generateLabelsApplication(Application $application, ?ApplicationPreview $preview = null): array
{ {
$ports = $application->settings->is_static ? [80] : $application->ports_exposes_array;
$onlyPort = null; $onlyPort = null;
if (count($ports) === 1) { if (count($ports) === 1) {
$onlyPort = $ports[0]; $onlyPort = $ports[0];
} }
$pull_request_id = data_get($preview, 'pull_request_id', 0); $pull_request_id = data_get($preview, 'pull_request_id', 0);
$container_name = generateApplicationContainerName($application, $pull_request_id); // $container_name = generateApplicationContainerName($application, $pull_request_id);
$appId = $application->id; $appId = $application->id;
if ($pull_request_id !== 0 && $pull_request_id !== null) { if ($pull_request_id !== 0 && $pull_request_id !== null) {
$appId = $appId . '-pr-' . $pull_request_id; $appId = $appId . '-pr-' . $pull_request_id;
} }
$labels = collect([]); $labels = collect([]);
$labels = $labels->merge(defaultLabels($appId, $container_name, $pull_request_id)); $labels = $labels->merge(defaultLabels($appId, $application->uuid, $pull_request_id));
if ($application->fqdn) { if ($application->fqdn) {
if ($pull_request_id !== 0) { if ($pull_request_id !== 0) {
$domains = Str::of(data_get($preview, 'fqdn'))->explode(','); $domains = Str::of(data_get($preview, 'fqdn'))->explode(',');
@@ -226,7 +226,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
$domains = Str::of(data_get($application, 'fqdn'))->explode(','); $domains = Str::of(data_get($application, 'fqdn'))->explode(',');
} }
// Add Traefik labels no matter which proxy is selected // Add Traefik labels no matter which proxy is selected
$labels = $labels->merge(fqdnLabelsForTraefik($domains, $application->settings->is_force_https_enabled,$onlyPort)); $labels = $labels->merge(fqdnLabelsForTraefik($application->uuid, $domains, $application->settings->is_force_https_enabled, $onlyPort));
} }
return $labels->all(); return $labels->all();
} }

View File

@@ -2,7 +2,6 @@
use App\Actions\Proxy\SaveConfiguration; use App\Actions\Proxy\SaveConfiguration;
use App\Models\Server; use App\Models\Server;
use App\Models\StandalonePostgresql;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
function get_proxy_path() function get_proxy_path()
@@ -21,7 +20,7 @@ function connectProxyToNetworks(Server $server)
} }
$commands = $networks->map(function ($network) { $commands = $networks->map(function ($network) {
return [ return [
"echo '####### Connecting coolify-proxy to $network network...'", "echo 'Connecting coolify-proxy to $network network...'",
"docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null || docker network create --attachable $network >/dev/null", "docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null || docker network create --attachable $network >/dev/null",
"docker network connect $network coolify-proxy >/dev/null 2>&1 || true", "docker network connect $network coolify-proxy >/dev/null 2>&1 || true",
]; ];
@@ -186,81 +185,3 @@ function setup_default_redirect_404(string|null $redirect_url, Server $server)
} }
} }
} }
function startPostgresProxy(StandalonePostgresql $database)
{
$containerName = "{$database->uuid}-proxy";
$configuration_dir = database_proxy_dir($database->uuid);
$nginxconf = <<<EOF
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
events {
worker_connections 1024;
}
stream {
server {
listen $database->public_port;
proxy_pass $database->uuid:5432;
}
}
EOF;
$dockerfile = <<< EOF
FROM nginx:stable-alpine
COPY nginx.conf /etc/nginx/nginx.conf
EOF;
$docker_compose = [
'version' => '3.8',
'services' => [
$containerName => [
'build' => [
'context' => $configuration_dir,
'dockerfile' => 'Dockerfile',
],
'image' => "nginx:stable-alpine",
'container_name' => $containerName,
'restart' => RESTART_MODE,
'ports' => [
"$database->public_port:$database->public_port",
],
'networks' => [
$database->destination->network,
],
'healthcheck' => [
'test' => [
'CMD-SHELL',
'stat /etc/nginx/nginx.conf || exit 1',
],
'interval' => '5s',
'timeout' => '5s',
'retries' => 3,
'start_period' => '1s'
],
]
],
'networks' => [
$database->destination->network => [
'external' => true,
'name' => $database->destination->network,
'attachable' => true,
]
]
];
$dockercompose_base64 = base64_encode(Yaml::dump($docker_compose, 4, 2));
$nginxconf_base64 = base64_encode($nginxconf);
$dockerfile_base64 = base64_encode($dockerfile);
instant_remote_process([
"mkdir -p $configuration_dir",
"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} up --build -d >/dev/null",
], $database->destination->server);
}
function stopPostgresProxy(StandalonePostgresql $database)
{
instant_remote_process(["docker rm -f {$database->uuid}-proxy"], $database->destination->server);
}

View File

@@ -108,12 +108,13 @@ function generateSshCommand(Server $server, string $command, bool $isMux = true)
} }
function instant_remote_process(Collection|array $command, Server $server, $throwError = true) function instant_remote_process(Collection|array $command, Server $server, $throwError = true)
{ {
$timeout = config('constants.ssh.command_timeout');
if ($command instanceof Collection) { if ($command instanceof Collection) {
$command = $command->toArray(); $command = $command->toArray();
} }
$command_string = implode("\n", $command); $command_string = implode("\n", $command);
$ssh_command = generateSshCommand($server, $command_string); $ssh_command = generateSshCommand($server, $command_string);
$process = Process::run($ssh_command); $process = Process::timeout($timeout)->run($ssh_command);
$output = trim($process->output()); $output = trim($process->output());
$exitCode = $process->exitCode(); $exitCode = $process->exitCode();
if ($exitCode !== 0) { if ($exitCode !== 0) {

View File

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

View File

@@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.75'; return '4.0.0-beta.93';

View File

@@ -23,6 +23,7 @@ return new class extends Migration
$table->string('git_repository'); $table->string('git_repository');
$table->string('git_branch'); $table->string('git_branch');
$table->string('git_commit_sha')->default('HEAD'); $table->string('git_commit_sha')->default('HEAD');
// TODO: remove this column, it is not used
$table->string('git_full_url')->nullable(); $table->string('git_full_url')->nullable();
$table->string('docker_registry_image_name')->nullable(); $table->string('docker_registry_image_name')->nullable();

View File

@@ -0,0 +1,54 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('standalone_redis', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->string('name');
$table->string('description')->nullable();
$table->text('redis_password');
$table->longText('redis_conf')->nullable();
$table->string('status')->default('exited');
$table->string('image')->default('redis:7.2');
$table->boolean('is_public')->default(false);
$table->integer('public_port')->nullable();
$table->text('ports_mappings')->nullable();
$table->string('limits_memory')->default("0");
$table->string('limits_memory_swap')->default("0");
$table->integer('limits_memory_swappiness')->default(60);
$table->string('limits_memory_reservation')->default("0");
$table->string('limits_cpus')->default("0");
$table->string('limits_cpuset')->nullable()->default("0");
$table->integer('limits_cpu_shares')->default(1024);
$table->timestamp('started_at')->nullable();
$table->morphs('destination');
$table->foreignId('environment_id')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('standalone_redis');
}
};

View File

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

View File

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

View File

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

View File

@@ -13,8 +13,8 @@ use App\Models\Server;
use App\Models\StandaloneDocker; use App\Models\StandaloneDocker;
use App\Models\Team; use App\Models\Team;
use App\Models\User; use App\Models\User;
use DB;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Process; use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
@@ -22,6 +22,12 @@ class ProductionSeeder extends Seeder
{ {
public function run(): void public function run(): void
{ {
if (isCloud()) {
echo "Running in cloud mode.\n";
} else {
echo "Running in self-hosted mode.\n";
}
// Fix for 4.0.0-beta.37 // Fix for 4.0.0-beta.37
if (User::find(0) !== null && Team::find(0) !== null) { if (User::find(0) !== null && Team::find(0) !== null) {
if (DB::table('team_user')->where('user_id', 0)->first() === null) { if (DB::table('team_user')->where('user_id', 0)->first() === null) {
@@ -60,7 +66,7 @@ class ProductionSeeder extends Seeder
]); ]);
} }
if (config('app.name') !== 'Coolify Cloud') { if (!isCloud()) {
// Save SSH Keys for the Coolify Host // Save SSH Keys for the Coolify Host
$coolify_key_name = "id.root@host.docker.internal"; $coolify_key_name = "id.root@host.docker.internal";
$coolify_key = Storage::disk('ssh-keys')->get("{$coolify_key_name}"); $coolify_key = Storage::disk('ssh-keys')->get("{$coolify_key_name}");

View File

@@ -16,10 +16,6 @@ class ServerSeeder extends Seeder
'ip' => "coolify-testing-host", 'ip' => "coolify-testing-host",
'team_id' => 0, 'team_id' => 0,
'private_key_id' => 0, 'private_key_id' => 0,
// 'proxy' => ServerMetadata::from([
// 'type' => ProxyTypes::TRAEFIK_V2->value,
// 'status' => ProxyStatus::EXITED->value
// ]),
]); ]);
} }
} }

View File

@@ -0,0 +1,23 @@
<?php
namespace Database\Seeders;
use App\Models\StandaloneDocker;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Illuminate\Database\Seeder;
class StandaloneRedisSeeder extends Seeder
{
public function run(): void
{
StandaloneRedis::create([
'name' => 'Local PostgreSQL',
'description' => 'Local PostgreSQL for testing',
'redis_password' => 'redis',
'environment_id' => 1,
'destination_id' => 0,
'destination_type' => StandaloneDocker::class,
]);
}
}

View File

@@ -27,4 +27,4 @@ RUN mkdir -p ~/.ssh
RUN echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFuGmoeGq/pojrsyP1pszcNVuZx9iFkCELtxrh31QJ68 coolify@coolify-instance" >> ~/.ssh/authorized_keys RUN echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFuGmoeGq/pojrsyP1pszcNVuZx9iFkCELtxrh31QJ68 coolify@coolify-instance" >> ~/.ssh/authorized_keys
EXPOSE 22 EXPOSE 22
CMD ["/usr/sbin/sshd", "-D", "-o", "ListenAddress=0.0.0.0"] CMD ["/usr/sbin/sshd", "-D", "-o", "ListenAddress=0.0.0.0", "-o", "Port=22"]

View File

@@ -1,2 +1,2 @@
User-agent: * User-agent: *
Disallow: Disallow: /

View File

@@ -7,14 +7,13 @@
href="{{ route('project.database.logs', $parameters) }}"> href="{{ route('project.database.logs', $parameters) }}">
<button>Logs</button> <button>Logs</button>
</a> </a>
<a class="{{ request()->routeIs('project.database.backups.all') ? 'text-white' : '' }}" @if ($database->getMorphClass() === 'App\Models\StandalonePostgresql')
href="{{ route('project.database.backups.all', $parameters) }}"> <a class="{{ request()->routeIs('project.database.backups.all') ? 'text-white' : '' }}"
<button>Backups</button> href="{{ route('project.database.backups.all', $parameters) }}">
</a> <button>Backups</button>
{{-- <x-applications.links :application="$application" /> --}} </a>
@endif
<div class="flex-1"></div> <div class="flex-1"></div>
{{-- <x-applications.advanced :application="$application" /> --}}
@if ($database->status !== 'exited') @if ($database->status !== 'exited')
<button wire:click='stop' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400"> <button wire:click='stop' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24" stroke-width="2" <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24" stroke-width="2"

View File

@@ -2,7 +2,9 @@
<livewire:server.proxy.modal :server="$server" /> <livewire:server.proxy.modal :server="$server" />
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h1>Server</h1> <h1>Server</h1>
<livewire:server.proxy.status :server="$server" /> @if ($server->proxyType() !== 'NONE')
<livewire:server.proxy.status :server="$server" />
@endif
</div> </div>
<div class="subtitle ">{{ data_get($server, 'name') }}</div> <div class="subtitle ">{{ data_get($server, 'name') }}</div>
<nav class="navbar-main"> <nav class="navbar-main">

View File

@@ -1,11 +1,11 @@
<!DOCTYPE html> <!DOCTYPE html>
<html data-theme="coollabs" lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <html data-theme="coollabs" lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preconnect" href="https://api.fonts.coollabs.io" crossorigin> <link rel="preconnect" href="https://api.fonts.coollabs.io" crossorigin>
<link href="https://api.fonts.coollabs.io/css2?family=Inter&display=swap" rel="stylesheet"> <link href="https://api.fonts.coollabs.io/css2?family=Inter&display=swap" rel="stylesheet">
<meta name="robots" content="noindex">
<title>Coolify</title> <title>Coolify</title>
@env('local') @env('local')
<link rel="icon" href="{{ asset('favicon-dev.png') }}" type="image/x-icon" /> <link rel="icon" href="{{ asset('favicon-dev.png') }}" type="image/x-icon" />

View File

@@ -14,9 +14,12 @@
<span>Your subscription has been activated! Welcome onboard!</span> <span>Your subscription has been activated! Welcome onboard!</span>
</div> </div>
@endif @endif
@if ($projects->count() === 0 && $servers->count() === 0)
<h3 class="pb-4">Projects</h3> No resources found. Add your first server / private key <a class="text-white underline" href="{{route('server.create')}}">here</a>.
@endif
@if ($projects->count() > 0)
<h3 class="pb-4">Projects</h3>
@endif
@if ($projects->count() === 1) @if ($projects->count() === 1)
<div class="grid grid-cols-1 gap-2"> <div class="grid grid-cols-1 gap-2">
@else @else
@@ -58,7 +61,9 @@
</div> </div>
@endforeach @endforeach
</div> </div>
<h3 class="py-4">Servers</h3> @if ($projects->count() > 0)
<h3 class="pb-4">Servers</h3>
@endif
@if ($servers->count() === 1) @if ($servers->count() === 1)
<div class="grid grid-cols-1 gap-2"> <div class="grid grid-cols-1 gap-2">
@else @else

View File

@@ -7,6 +7,6 @@
@endif @endif
@if (data_get($application_deployment_queue, 'status') === 'in_progress' || @if (data_get($application_deployment_queue, 'status') === 'in_progress' ||
data_get($application_deployment_queue, 'status') === 'queued') data_get($application_deployment_queue, 'status') === 'queued')
<x-forms.button wire:click.prevent="cancel">Cancel deployment</x-forms.button> <x-forms.button wire:click.prevent="cancel">Cancel Deployment</x-forms.button>
@endif @endif
</div> </div>

View File

@@ -5,8 +5,12 @@
<x-forms.button type="submit"> <x-forms.button type="submit">
Save Save
</x-forms.button> </x-forms.button>
@if ($isConfigurationChanged && !is_null($application->config_hash))
<div class="font-bold text-warning">Configuration not applied to the running application. You need to
redeploy.</div>
@endif
</div> </div>
<div class="">General configuration for your application.</div> <div>General configuration for your application.</div>
<div class="flex flex-col gap-2 py-4"> <div class="flex flex-col gap-2 py-4">
<div class="flex flex-col items-end gap-2 xl:flex-row"> <div class="flex flex-col items-end gap-2 xl:flex-row">
<x-forms.input id="application.name" label="Name" required /> <x-forms.input id="application.name" label="Name" required />
@@ -21,7 +25,7 @@
@if (!$application->dockerfile) @if (!$application->dockerfile)
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.select id="application.build_pack" label="Build Pack" required> <x-forms.select wire:model="application.build_pack" label="Build Pack" required>
<option value="nixpacks">Nixpacks</option> <option value="nixpacks">Nixpacks</option>
<option value="dockerfile">Dockerfile</option> <option value="dockerfile">Dockerfile</option>
<option value="dockerimage">Docker Image</option> <option value="dockerimage">Docker Image</option>
@@ -73,15 +77,14 @@
</div> </div>
@else @else
<div class="flex flex-col gap-2 xl:flex-row"> <div class="flex flex-col gap-2 xl:flex-row">
<x-forms.input id="application.docker_registry_image_name" required label="Docker Image" /> <x-forms.input id="application.docker_registry_image_name" label="Docker Image" />
<x-forms.input id="application.docker_registry_image_tag" required label="Docker Image Tag" /> <x-forms.input id="application.docker_registry_image_tag" label="Docker Image Tag" />
</div> </div>
@endif @endif
@if ($application->dockerfile) @if ($application->dockerfile)
<x-forms.textarea label="Dockerfile" id="application.dockerfile" rows="6"> </x-forms.textarea> <x-forms.textarea label="Dockerfile" id="application.dockerfile" rows="6"> </x-forms.textarea>
@endif @endif
<h3>Network</h3> <h3>Network</h3>
<div class="flex flex-col gap-2 xl:flex-row"> <div class="flex flex-col gap-2 xl:flex-row">
@if ($application->settings->is_static) @if ($application->settings->is_static)
@@ -93,6 +96,12 @@
<x-forms.input placeholder="3000:3000" id="application.ports_mappings" label="Ports Mappings" <x-forms.input placeholder="3000:3000" id="application.ports_mappings" label="Ports Mappings"
helper="A comma separated list of ports you would like to map to the host system. Useful when you do not want to use domains.<br><br><span class='inline-block font-bold text-warning'>Example:</span><br>3000:3000,3002:3002<br><br>Rolling update is not supported if you have a port mapped to the host." /> helper="A comma separated list of ports you would like to map to the host system. Useful when you do not want to use domains.<br><br><span class='inline-block font-bold text-warning'>Example:</span><br>3000:3000,3002:3002<br><br>Rolling update is not supported if you have a port mapped to the host." />
</div> </div>
@if ($labelsChanged)
<x-forms.textarea label="Custom Labels" rows="15" id="customLabels"></x-forms.textarea>
@else
<x-forms.textarea label="Coolify Generated Labels" rows="15" id="customLabels"></x-forms.textarea>
@endif
<x-forms.button wire:click="resetDefaultLabels">Reset to Coolify Generated Labels</x-forms.button>
</div> </div>
<h3>Advanced</h3> <h3>Advanced</h3>
<div class="flex flex-col"> <div class="flex flex-col">

View File

@@ -36,7 +36,7 @@
</div> </div>
@isset($application->private_key_id) @isset($application->private_key_id)
<h3 class="pt-4">Deploy Key</h3> <h3 class="pt-4">Deploy Key</h3>
<div class="py-2 pt-4">Currently attache Private Key: <span <div class="py-2 pt-4">Currently attached Private Key: <span
class="text-warning">{{ $application->private_key->name }}</span> class="text-warning">{{ $application->private_key->name }}</span>
</div> </div>

View File

@@ -26,6 +26,7 @@
</div> </div>
@endif @endif
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.input label="Databases To Backup" helper="Comma separated list of databases to backup. Empty will include the default one." id="backup.databases_to_backup" />
<x-forms.input label="Frequency" id="backup.frequency" /> <x-forms.input label="Frequency" id="backup.frequency" />
<x-forms.input label="Number of backups to keep (locally)" id="backup.number_of_backups_locally" /> <x-forms.input label="Number of backups to keep (locally)" id="backup.number_of_backups_locally" />
</div> </div>

View File

@@ -1,18 +1,19 @@
<div class="flex flex-col flex-col-reverse gap-2"> <div class="flex flex-col-reverse gap-2">
@forelse($executions as $execution) @forelse($executions as $execution)
<form class="flex flex-col p-2 border-dotted border-1 bg-coolgray-300" @class([ <form class="flex flex-col p-2 border-dotted border-1 bg-coolgray-300" @class([
'border-green-500' => data_get($execution, 'status') === 'success', 'border-green-500' => data_get($execution, 'status') === 'success',
'border-red-500' => data_get($execution, 'status') === 'failed', 'border-red-500' => data_get($execution, 'status') === 'failed',
])> ])>
<div>Started At: {{ data_get($execution, 'created_at') }}</div> <div>Database: {{ data_get($execution, 'database_name', 'N/A') }}</div>
<div>Status: {{ data_get($execution, 'status') }}</div> <div>Status: {{ data_get($execution, 'status') }}</div>
<div>Started At: {{ data_get($execution, 'created_at') }}</div>
@if (data_get($execution, 'message')) @if (data_get($execution, 'message'))
<div>Message: {{ data_get($execution, 'message') }}</div> <div>Message: {{ data_get($execution, 'message') }}</div>
@endif @endif
<div>Size: {{ data_get($execution, 'size') }} B / {{ round((int) data_get($execution, 'size') / 1024, 2) }} <div>Size: {{ data_get($execution, 'size') }} B / {{ round((int) data_get($execution, 'size') / 1024, 2) }}
kB / {{ round((int) data_get($execution, 'size') / 1024 / 1024, 2) }} MB kB / {{ round((int) data_get($execution, 'size') / 1024 / 1024, 3) }} MB
</div> </div>
<div>Location: {{ data_get($execution, 'filename') }}</div> <div>Location: {{ data_get($execution, 'filename', 'N/A') }}</div>
<livewire:project.database.backup-execution :execution="$execution" :wire:key="$execution->id" /> <livewire:project.database.backup-execution :execution="$execution" :wire:key="$execution->id" />
</form> </form>
@empty @empty

View File

@@ -62,7 +62,7 @@
label="Public Port" /> label="Public Port" />
<x-forms.checkbox instantSave id="database.is_public" label="Accessible over the internet" /> <x-forms.checkbox instantSave id="database.is_public" label="Accessible over the internet" />
</div> </div>
<x-forms.input label="Postgres URL" readonly wire:model="db_url" /> <x-forms.input label="Postgres URL" type="password" readonly wire:model="db_url" />
</div> </div>
</form> </form>
<div class="pb-16"> <div class="pb-16">

View File

@@ -0,0 +1,28 @@
<div>
<form wire:submit.prevent="submit" class="flex flex-col gap-2">
<div class="flex items-center gap-2">
<h2>General</h2>
<x-forms.button type="submit">
Save
</x-forms.button>
</div>
<div class="flex gap-2">
<x-forms.input label="Name" id="database.name" />
<x-forms.input label="Description" id="database.description" />
<x-forms.input label="Image" id="database.image" required
helper="For all available images, check here:<br><br><a target='_blank' href='https://hub.docker.com/_/redis'>https://hub.docker.com/_/redis</a>" />
</div>
<div class="flex flex-col gap-2">
<h3 class="py-2">Network</h3>
<div class="flex items-end gap-2">
<x-forms.input placeholder="3000:5432" id="database.ports_mappings" label="Ports Mappings"
helper="A comma separated list of ports you would like to map to the host system.<br><span class='inline-block font-bold text-warning'>Example</span>3000:5432,3002:5433" />
<x-forms.input placeholder="5432" disabled="{{ $database->is_public }}" id="database.public_port"
label="Public Port" />
<x-forms.checkbox instantSave id="database.is_public" label="Accessible over the internet" />
</div>
<x-forms.input label="Redis URL" type="password" readonly wire:model="db_url" />
</div>
<x-forms.textarea helper="<a target='_blank' class='text-white underline' href='https://raw.githubusercontent.com/redis/redis/7.2/redis.conf'>Redis Default Configuration</a>" label="Custom Redis Configuration" rows="10" id="database.redis_conf" />
</form>
</div>

View File

@@ -1,13 +1,13 @@
<div> <div>
<h1>Create a new Application</h1> <h1>Create a new Application</h1>
<div class="pb-4">Deploy any public Git repositories.</div> <div class="pb-4">Deploy any public Git repositories.</div>
<form class="flex flex-col gap-2" wire:submit.prevent> <form class="flex flex-col gap-2" wire:submit.prevent='load_branch'>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex items-end gap-2"> <div class="flex items-end gap-2">
<x-forms.input wire:keydown.enter='load_branch' id="repository_url" label="Repository URL" <x-forms.input required id="repository_url" label="Repository URL"
helper="{!! __('repository.url') !!}" /> helper="{!! __('repository.url') !!}" />
<x-forms.button wire:click.prevent="load_branch"> <x-forms.button type="submit">
Check repository Check repository
</x-forms.button> </x-forms.button>
</div> </div>
@@ -16,10 +16,9 @@
<div class="flex gap-2 py-2"> <div class="flex gap-2 py-2">
<div>Rate Limit</div> <div>Rate Limit</div>
<x-helper <x-helper
helper="Rate limit remaining: {{ $rate_limit_remaining }}<br>Rate limit reset at: {{ $rate_limit_reset }}" /> helper="Rate limit remaining: {{ $rate_limit_remaining }}<br>Rate limit reset at: {{ $rate_limit_reset }} UTC" />
</div> </div>
@endif @endif
<h3 class="pt-8 pb-2">Details</h3>
<div class="flex flex-col gap-2 pb-6"> <div class="flex flex-col gap-2 pb-6">
<div class="flex gap-2"> <div class="flex gap-2">
@if ($git_source === 'other') @if ($git_source === 'other')

View File

@@ -25,7 +25,7 @@
Public Repository Public Repository
</div> </div>
<div class="text-xs group-hover:text-white"> <div class="text-xs group-hover:text-white">
You can deploy any kind of public repositories from the supported git servers. You can deploy any kind of public repositories from the supported git providers.
</div> </div>
</div> </div>
</div> </div>
@@ -94,6 +94,16 @@
</div> </div>
</div> </div>
</div> </div>
<div class="box group" wire:click="setType('redis')">
<div class="flex flex-col mx-6">
<div class="font-bold text-white group-hover:text-white">
New Redis
</div>
<div class="text-xs group-hover:text-white">
The open source, in-memory data store for cache, streaming engine, and message broker.
</div>
</div>
</div>
{{-- <div class="box group" wire:click="setType('existing-postgresql')"> {{-- <div class="box group" wire:click="setType('existing-postgresql')">
<div class="flex flex-col mx-6"> <div class="flex flex-col mx-6">
<div class="group-hover:text-white"> <div class="group-hover:text-white">
@@ -142,6 +152,9 @@
@endforeach @endforeach
@endif @endif
</div> </div>
<div class="py-4 pb-10">Trademarks Policy: The respective trademarks mentioned here are owned by the
respective
companies, and use of them does not imply any affiliation or endorsement.</div>
@endif @endif
@if ($current_step === 'servers') @if ($current_step === 'servers')
<ul class="pb-10 steps"> <ul class="pb-10 steps">

View File

@@ -18,10 +18,10 @@
<div class="flex gap-2"> <div class="flex gap-2">
@if ($application->required_fqdn) @if ($application->required_fqdn)
<x-forms.input required placeholder="https://app.coolify.io" label="Domains" <x-forms.input required placeholder="https://app.coolify.io" label="Domains"
id="application.fqdn"></x-forms.input> id="application.fqdn" helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io, https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input>
@else @else
<x-forms.input placeholder="https://app.coolify.io" label="Domains" <x-forms.input placeholder="https://app.coolify.io" label="Domains"
id="application.fqdn"></x-forms.input> id="application.fqdn" helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io, https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input>
@endif @endif
<x-forms.input required <x-forms.input required
helper="You can change the image you would like to deploy.<br><br><span class='text-warning'>WARNING. You could corrupt your data. Only do it if you know what you are doing.</span>" helper="You can change the image you would like to deploy.<br><br><span class='text-warning'>WARNING. You could corrupt your data. Only do it if you know what you are doing.</span>"

View File

@@ -1,40 +1,24 @@
<div x-data="{ raw: true, activeTab: window.location.hash ? window.location.hash.substring(1) : 'service-stack' }" wire:poll.2000ms="checkStatus"> <div x-data="{ raw: true, activeTab: window.location.hash ? window.location.hash.substring(1) : 'service-stack' }" wire:poll.15000ms="checkStatus">
<livewire:project.service.navbar :service="$service" :parameters="$parameters" :query="$query" /> <livewire:project.service.navbar :service="$service" :parameters="$parameters" :query="$query" />
<livewire:project.service.compose-modal :raw="$service->docker_compose_raw" :actual="$service->docker_compose" /> <livewire:project.service.compose-modal :raw="$service->docker_compose_raw" :actual="$service->docker_compose" />
<div class="flex h-full pt-6"> <div class="flex h-full pt-6">
<div class="flex flex-col items-start gap-4 min-w-fit"> <div class="flex flex-col items-start gap-4 min-w-fit">
<a target="_blank" href="{{ $service->documentation() }}">Documentation <x-external-link /></a> <a target="_blank" href="{{ $service->documentation() }}">Documentation <x-external-link /></a>
<a :class="activeTab === 'service-stack' && 'text-white'" <a :class="activeTab === 'service-stack' && 'text-white'" @click.prevent="activeTab = 'service-stack';
@click.prevent="activeTab = 'service-stack'; window.location.hash = 'service-stack'" window.location.hash = 'service-stack'" href="#">Service Stack</a>
href="#">Service Stack</a> <a :class="activeTab === 'storages' && 'text-white'" @click.prevent="activeTab = 'storages';
<a :class="activeTab === 'storages' && 'text-white'" window.location.hash = 'storages'" href="#">Storages</a>
@click.prevent="activeTab = 'storages'; window.location.hash = 'storages'" href="#">Storages</a>
<a :class="activeTab === 'environment-variables' && 'text-white'" <a :class="activeTab === 'environment-variables' && 'text-white'"
@click.prevent="activeTab = 'environment-variables'; window.location.hash = 'environment-variables'" @click.prevent="activeTab = 'environment-variables'; window.location.hash = 'environment-variables'"
href="#">Environment href="#">Environment
Variables</a> Variables</a>
<a :class="activeTab === 'danger' && 'text-white'" <a :class="activeTab === 'danger' && 'text-white'" @click.prevent="activeTab = 'danger';
@click.prevent="activeTab = 'danger'; window.location.hash = 'danger'" href="#">Danger Zone window.location.hash = 'danger'" href="#">Danger Zone
</a> </a>
</div> </div>
<div class="w-full pl-8"> <div class="w-full pl-8">
<div x-cloak x-show="activeTab === 'service-stack'"> <div x-cloak x-show="activeTab === 'service-stack'">
<form wire:submit.prevent='submit' class="flex flex-col gap-4 pb-2"> <livewire:project.service.stack-form :service="$service" />
<div class="flex gap-2">
<div>
<h2> Service Stack </h2>
<div>Configuration</div>
</div>
<x-forms.button type="submit">Save</x-forms.button>
<x-forms.button class="w-64" onclick="composeModal.showModal()">Edit Compose
File</x-forms.button>
</div>
<div class="flex gap-2">
<x-forms.input id="service.name" required label="Service Name"
placeholder="My super wordpress site" />
<x-forms.input id="service.description" label="Description" />
</div>
</form>
<div class="grid grid-cols-1 gap-2 pt-4 xl:grid-cols-3"> <div class="grid grid-cols-1 gap-2 pt-4 xl:grid-cols-3">
@foreach ($applications as $application) @foreach ($applications as $application)
<div @class([ <div @class([
@@ -66,7 +50,8 @@
<div class="text-xs">{{ $application->status }}</div> <div class="text-xs">{{ $application->status }}</div>
</a> </a>
<a class="flex gap-2 p-1 mx-4 font-bold rounded group-hover:text-white hover:no-underline" <a class="flex gap-2 p-1 mx-4 font-bold rounded group-hover:text-white hover:no-underline"
href="{{ route('project.service.logs', [...$parameters, 'service_name' => $application->name]) }}"><span class="hover:text-warning">Logs</span></a> href="{{ route('project.service.logs', [...$parameters, 'service_name' => $application->name]) }}"><span
class="hover:text-warning">Logs</span></a>
</div> </div>
@endforeach @endforeach
@foreach ($databases as $database) @foreach ($databases as $database)
@@ -95,7 +80,8 @@
<div class="text-xs">{{ $database->status }}</div> <div class="text-xs">{{ $database->status }}</div>
</a> </a>
<a class="flex gap-2 p-1 mx-4 font-bold rounded hover:no-underline group-hover:text-white" <a class="flex gap-2 p-1 mx-4 font-bold rounded hover:no-underline group-hover:text-white"
href="{{ route('project.service.logs', [...$parameters, 'service_name' => $database->name]) }}"><span class="hover:text-warning">Logs</span></a> href="{{ route('project.service.logs', [...$parameters, 'service_name' => $database->name]) }}"><span
class="hover:text-warning">Logs</span></a>
</div> </div>
@endforeach @endforeach
</div> </div>
@@ -126,3 +112,4 @@
</div> </div>
</div> </div>
</div> </div>
</div>

View File

@@ -1,5 +1,5 @@
<div> <div>
<x-modal submitWireAction="serviceStatusUpdated" modalId="startService"> <x-modal submitWireAction="checkStatus" modalId="startService">
<x-slot:modalBody> <x-slot:modalBody>
<livewire:activity-monitor header="Service Startup Logs" /> <livewire:activity-monitor header="Service Startup Logs" />
</x-slot:modalBody> </x-slot:modalBody>

View File

@@ -1,4 +1,4 @@
<div x-init="$wire.check_status"> <div x-init="$wire.checkStatus" wire:poll.2500ms='checkStatus'>
<livewire:project.service.modal /> <livewire:project.service.modal />
<h1>Configuration</h1> <h1>Configuration</h1>
<x-resources.breadcrumbs :resource="$service" :parameters="$parameters" /> <x-resources.breadcrumbs :resource="$service" :parameters="$parameters" />

View File

@@ -0,0 +1,16 @@
<form wire:submit.prevent='submit' class="flex flex-col gap-4 pb-2">
<div class="flex gap-2">
<div>
<h2>Service Stack</h2>
<div>Configuration</div>
</div>
<x-forms.button type="submit">Save</x-forms.button>
<x-forms.button class="w-64" onclick="composeModal.showModal()">Edit Compose
File</x-forms.button>
</div>
<div class="flex gap-2">
<x-forms.input id="service.name" required label="Service Name"
placeholder="My super wordpress site" />
<x-forms.input id="service.description" label="Description" />
</div>
</form>

View File

@@ -1,7 +1,8 @@
<div> <div>
@if ( @if (
$resource->getMorphClass() == 'App\Models\Application' || $resource->getMorphClass() == 'App\Models\Application' ||
$resource->getMorphClass() == 'App\Models\StandalonePostgresql') $resource->getMorphClass() == 'App\Models\StandalonePostgresql' ||
$resource->getMorphClass() == 'App\Models\StandaloneRedis')
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h2>Storages</h2> <h2>Storages</h2>
<x-helper <x-helper

View File

@@ -9,11 +9,7 @@
<h1>Logs</h1> <h1>Logs</h1>
<livewire:project.database.heading :database="$resource" /> <livewire:project.database.heading :database="$resource" />
<div class="pt-4"> <div class="pt-4">
@if (Str::of($status)->startsWith('running')) <livewire:project.shared.get-logs :server="$server" :container="$container" />
<livewire:project.shared.get-logs :server="$server" :container="$container" />
@else
Database is not running.
@endif
</div> </div>
@elseif ($type === 'service') @elseif ($type === 'service')
<livewire:project.service.navbar :service="$resource" :parameters="$parameters" :query="$query" /> <livewire:project.service.navbar :service="$resource" :parameters="$parameters" :query="$query" />

View File

@@ -26,16 +26,23 @@
Server is reachable and validated. Server is reachable and validated.
@endif @endif
@if ((!$server->settings->is_reachable || !$server->settings->is_usable) && $server->id !== 0) @if ((!$server->settings->is_reachable || !$server->settings->is_usable) && $server->id !== 0)
<x-forms.button class="mt-8 mb-4 font-bold box-without-bg bg-coollabs hover:bg-coollabs-100" wire:click.prevent='validateServer' isHighlighted> <x-forms.button class="mt-8 mb-4 font-bold box-without-bg bg-coollabs hover:bg-coollabs-100"
wire:click.prevent='validateServer' isHighlighted>
Validate Server & Install Docker Engine Validate Server & Install Docker Engine
</x-forms.button> </x-forms.button>
@endif @endif
@if ((!$server->settings->is_reachable || !$server->settings->is_usable) && $server->id === 0)
<x-forms.button class="mt-8 mb-4 font-bold box-without-bg bg-coollabs hover:bg-coollabs-100"
wire:click.prevent='checkLocalhostConnection' isHighlighted>
Validate Server
</x-forms.button>
@endif
<div class="flex flex-col gap-2 pt-4"> <div class="flex flex-col gap-2 pt-4">
<div class="flex flex-col w-full gap-2 lg:flex-row"> <div class="flex flex-col w-full gap-2 lg:flex-row">
<x-forms.input id="server.name" label="Name" required /> <x-forms.input id="server.name" label="Name" required />
<x-forms.input id="server.description" label="Description" /> <x-forms.input id="server.description" label="Description" />
<x-forms.input placeholder="https://example.com" id="wildcard_domain" label="Wildcard Domain" <x-forms.input placeholder="https://example.com" id="wildcard_domain" label="Wildcard Domain"
helper="Wildcard domain for your applications. If you set this, you will get a random generated domain for your new applications.<br><span class='font-bold text-white'>Example</span>In case you set:<span class='text-helper'>https://example.com</span>your applications will get: <span class='text-helper'>https://randomId.example.com</span>" /> helper="Wildcard domain for your applications. If you set this, you will get a random generated domain for your new applications.<br><span class='font-bold text-white'>Example:</span><br>In case you set:<span class='text-helper'>https://example.com</span> your applications will get:<br> <span class='text-helper'>https://randomId.example.com</span>" />
{{-- <x-forms.checkbox disabled type="checkbox" id="server.settings.is_part_of_swarm" {{-- <x-forms.checkbox disabled type="checkbox" id="server.settings.is_part_of_swarm"
label="Is it part of a Swarm cluster?" /> --}} label="Is it part of a Swarm cluster?" /> --}}
</div> </div>
@@ -59,13 +66,13 @@
helper="Disk cleanup job will be executed if disk usage is more than this number." /> helper="Disk cleanup job will be executed if disk usage is more than this number." />
@endif @endif
</form> </form>
<h2 class="pt-4">Danger Zone</h2> @if ($server->id !== 0)
<div class="">Woah. I hope you know what are you doing.</div> <h2 class="pt-4">Danger Zone</h2>
<h4 class="pt-4">Delete Server</h4> <div class="">Woah. I hope you know what are you doing.</div>
<div class="pb-4">This will remove this server from Coolify. Beware! There is no coming <h4 class="pt-4">Delete Server</h4>
back! <div class="pb-4">This will remove this server from Coolify. Beware! There is no coming
</div> back!
@if ($server->id !== 0 || isDev()) </div>
<x-forms.button isError isModal modalId="deleteServer"> <x-forms.button isError isModal modalId="deleteServer">
Delete Delete
</x-forms.button> </x-forms.button>

View File

@@ -12,7 +12,7 @@
<div class="flex gap-4"> <div class="flex gap-4">
@if ($currentRoute === 'server.proxy' && $traefikDashboardAvailable) @if ($currentRoute === 'server.proxy' && $traefikDashboardAvailable)
<button> <button>
<a target="_blank" href="http://{{ $server->ip }}:8080"> <a target="_blank" href="http://{{ $serverIp }}:8080">
Traefik Dashboard Traefik Dashboard
<x-external-link /> <x-external-link />
</a> </a>
@@ -20,8 +20,9 @@
@endif @endif
<x-forms.button isModal noStyle modalId="stopProxy" <x-forms.button isModal noStyle modalId="stopProxy"
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400"> class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24" stroke-width="2" <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path> <path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
<path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path> <path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
@@ -30,7 +31,7 @@
</x-forms.button> </x-forms.button>
</div> </div>
@else @else
<button wire:click='startProxy' onclick="startProxy.showModal()" <button onclick="checkProxy()"
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400"> class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-warning" viewBox="0 0 24 24" <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-warning" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
@@ -42,4 +43,13 @@
</button> </button>
@endif @endif
@endif @endif
<script>
function checkProxy() {
Livewire.emit('checkProxy')
}
Livewire.on('proxyChecked', () => {
startProxy.showModal();
Livewire.emit('startProxy');
})
</script>
</div> </div>

View File

@@ -1,6 +1,6 @@
<div> <div>
@if ($server->isFunctional()) @if ($server->isFunctional())
<div class="flex gap-2" x-init="$wire.getProxyStatus"> <div class="flex gap-2" @if ($polling) wire:poll.2000ms='checkProxy' @endif>
@if (data_get($server, 'proxy.status') === 'running') @if (data_get($server, 'proxy.status') === 'running')
<x-status.running status="Proxy Running" /> <x-status.running status="Proxy Running" />
@elseif (data_get($server, 'proxy.status') === 'restarting') @elseif (data_get($server, 'proxy.status') === 'restarting')
@@ -8,7 +8,9 @@
@else @else
<x-status.stopped status="Proxy Stopped" /> <x-status.stopped status="Proxy Stopped" />
@endif @endif
<x-forms.button wire:click='getProxyStatusWithNoti'>Refresh </x-forms.button> @if (data_get($server, 'proxy.status') === 'running')
<x-forms.button wire:click='checkProxy(true)'>Refresh</x-forms.button>
@endif
</div> </div>
@endif @endif
</div> </div>

View File

@@ -13,25 +13,23 @@
</x-modal> </x-modal>
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'general' }" class="flex h-full pt-6"> <div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'general' }" class="flex h-full pt-6">
<div class="flex flex-col gap-4 min-w-fit"> <div class="flex flex-col gap-4 min-w-fit">
<a :class="activeTab === 'general' && 'text-white'" <a :class="activeTab === 'general' && 'text-white'" @click.prevent="activeTab = 'general';
@click.prevent="activeTab = 'general'; window.location.hash = 'general'" href="#">General</a> window.location.hash = 'general'" href="#">General</a>
<a :class="activeTab === 'environment-variables' && 'text-white'" <a :class="activeTab === 'environment-variables' && 'text-white'"
@click.prevent="activeTab = 'environment-variables'; window.location.hash = 'environment-variables'" @click.prevent="activeTab = 'environment-variables'; window.location.hash = 'environment-variables'"
href="#">Environment href="#">Environment
Variables</a> Variables</a>
<a :class="activeTab === 'server' && 'text-white'" <a :class="activeTab === 'server' && 'text-white'" @click.prevent="activeTab = 'server';
@click.prevent="activeTab = 'server'; window.location.hash = 'server'" window.location.hash = 'server'" href="#">Server
href="#">Server
</a> </a>
<a :class="activeTab === 'storages' && 'text-white'" <a :class="activeTab === 'storages' && 'text-white'" @click.prevent="activeTab = 'storages';
@click.prevent="activeTab = 'storages'; window.location.hash = 'storages'" href="#">Storages window.location.hash = 'storages'" href="#">Storages
</a> </a>
<a :class="activeTab === 'resource-limits' && 'text-white'" <a :class="activeTab === 'resource-limits' && 'text-white'" @click.prevent="activeTab = 'resource-limits';
@click.prevent="activeTab = 'resource-limits'; window.location.hash = 'resource-limits'" window.location.hash = 'resource-limits'" href="#">Resource Limits
href="#">Resource Limits
</a> </a>
<a :class="activeTab === 'danger' && 'text-white'" <a :class="activeTab === 'danger' && 'text-white'" @click.prevent="activeTab = 'danger';
@click.prevent="activeTab = 'danger'; window.location.hash = 'danger'" href="#">Danger Zone window.location.hash = 'danger'" href="#">Danger Zone
</a> </a>
</div> </div>
<div class="w-full pl-8"> <div class="w-full pl-8">
@@ -39,6 +37,9 @@
@if ($database->type() === 'standalone-postgresql') @if ($database->type() === 'standalone-postgresql')
<livewire:project.database.postgresql.general :database="$database" /> <livewire:project.database.postgresql.general :database="$database" />
@endif @endif
@if ($database->type() === 'standalone-redis')
<livewire:project.database.redis.general :database="$database" />
@endif
</div> </div>
<div x-cloak x-show="activeTab === 'environment-variables'"> <div x-cloak x-show="activeTab === 'environment-variables'">
<livewire:project.shared.environment-variable.all :resource="$database" /> <livewire:project.shared.environment-variable.all :resource="$database" />

View File

@@ -38,30 +38,52 @@
@endif @endif
<div class="grid gap-2 lg:grid-cols-2"> <div class="grid gap-2 lg:grid-cols-2">
@foreach ($environment->applications->sortBy('name') as $application) @foreach ($environment->applications->sortBy('name') as $application)
<a class="box group" <a class="relative box group"
href="{{ route('project.application.configuration', [$project->uuid, $environment->name, $application->uuid]) }}"> href="{{ route('project.application.configuration', [$project->uuid, $environment->name, $application->uuid]) }}">
<div class="flex flex-col mx-6"> <div class="flex flex-col mx-6">
<div class="font-bold text-white">{{ $application->name }}</div> <div class="font-bold text-white">{{ $application->name }}</div>
<div class="text-xs text-gray-400 group-hover:text-white">{{ $application->description }}</div> <div class="text-xs text-gray-400 group-hover:text-white">{{ $application->description }}</div>
</div> </div>
@if (Str::of(data_get($application, 'status'))->startsWith('running'))
<div class="absolute bg-success -top-1 -left-1 badge badge-xs"></div>
@elseif (Str::of(data_get($application, 'status'))->startsWith('exited'))
<div class="absolute bg-error -top-1 -left-1 badge badge-xs"></div>
@elseif (Str::of(data_get($application, 'status'))->startsWith('restarting'))
<div class="absolute bg-warning -top-1 -left-1 badge badge-xs"></div>
@endif
</a> </a>
@endforeach @endforeach
@foreach ($environment->databases->sortBy('name') as $databases) @foreach ($environment->databases()->sortBy('name') as $database)
<a class="box group" <a class="relative box group"
href="{{ route('project.database.configuration', [$project->uuid, $environment->name, $databases->uuid]) }}"> href="{{ route('project.database.configuration', [$project->uuid, $environment->name, $database->uuid]) }}">
<div class="flex flex-col mx-6"> <div class="flex flex-col mx-6">
<div class="font-bold text-white">{{ $databases->name }}</div> <div class="font-bold text-white">{{ $database->name }}</div>
<div class="text-xs text-gray-400 group-hover:text-white">{{ $databases->description }}</div> <div class="text-xs text-gray-400 group-hover:text-white">{{ $database->description }}</div>
</div> </div>
@if (Str::of(data_get($database, 'status'))->startsWith('running'))
<div class="absolute bg-success -top-1 -left-1 badge badge-xs"></div>
@elseif (Str::of(data_get($database, 'status'))->startsWith('exited'))
<div class="absolute bg-error -top-1 -left-1 badge badge-xs"></div>
@elseif (Str::of(data_get($database, 'status'))->startsWith('restaring'))
<div class="absolute bg-warning -top-1 -left-1 badge badge-xs"></div>
@endif
</a> </a>
@endforeach @endforeach
@foreach ($environment->services->sortBy('name') as $service) @foreach ($environment->services->sortBy('name') as $service)
<a class="box group" <a class="relative box group"
href="{{ route('project.service', [$project->uuid, $environment->name, $service->uuid]) }}"> href="{{ route('project.service', [$project->uuid, $environment->name, $service->uuid]) }}">
<div class="flex flex-col mx-6"> <div class="flex flex-col mx-6">
<div class="font-bold text-white">{{ $service->name }}</div> <div class="font-bold text-white">{{ $service->name }}</div>
<div class="text-xs text-gray-400 group-hover:text-white">{{ $service->description }}</div> <div class="text-xs text-gray-400 group-hover:text-white">{{ $service->description }}</div>
</div> </div>
@if (Str::of(serviceStatus($service))->startsWith('running'))
<div class="absolute bg-success -top-1 -left-1 badge badge-xs"></div>
@elseif (Str::of(serviceStatus($service))->startsWith('degraded'))
<div class="absolute bg-warning -top-1 -left-1 badge badge-xs"></div>
@elseif (Str::of(serviceStatus($service))->startsWith('exited'))
<div class="absolute bg-error -top-1 -left-1 badge badge-xs"></div>
@endif
</a> </a>
@endforeach @endforeach
</div> </div>

View File

@@ -73,6 +73,7 @@ Route::get('/verify', function () {
Route::get('/email/verify/{id}/{hash}', function (EmailVerificationRequest $request) { Route::get('/email/verify/{id}/{hash}', function (EmailVerificationRequest $request) {
$request->fulfill(); $request->fulfill();
send_internal_notification("User {$request->user()->name} verified their email address.");
return redirect('/'); return redirect('/');
})->middleware(['auth'])->name('verify.verify'); })->middleware(['auth'])->name('verify.verify');

View File

@@ -18,7 +18,7 @@ if [ $EUID != 0 ]; then
echo "Please run as root" echo "Please run as root"
exit exit
fi fi
if [ $OS_TYPE != "ubuntu" ] && [ $OS_TYPE != "debian" ]; then if [ $OS_TYPE != "ubuntu" ] && [ $OS_TYPE != "debian" ] && [ $OS_TYPE != "raspbian" ]; then
echo "This script only supports Ubuntu and Debian for now." echo "This script only supports Ubuntu and Debian for now."
exit exit
fi fi

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