Compare commits

...

67 Commits

Author SHA1 Message Date
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
88 changed files with 1429 additions and 539 deletions

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,42 @@
<?php
namespace App\Actions\Proxy;
use App\Enums\ProxyTypes;
use App\Models\Server;
use Illuminate\Support\Str;
use Lorisleiva\Actions\Concerns\AsAction;
use Spatie\Activitylog\Models\Activity;
class CheckProxy
{
use AsAction;
public function handle(Server $server)
{
if (!$server->isProxyShouldRun()) {
throw new \Exception("Proxy should not run");
}
$status = getContainerStatus($server, 'coolify-proxy');
if ($status === 'running') {
$server->proxy->set('status', 'running');
$server->save();
return 'OK';
}
$ip = $server->ip;
if ($server->id === 0) {
$ip = 'host.docker.internal';
}
$connection = @fsockopen($ip, '80');
$connection = @fsockopen($ip, '443');
$port80 = is_resource($connection) && fclose($connection);
$port443 = is_resource($connection) && fclose($connection);
ray($ip);
if ($port80) {
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>");
}
if ($port443) {
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>>");
}
}
}

View File

@@ -10,56 +10,49 @@ 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(); CheckProxy::run($server);
if ($proxyType === 'none') {
return 'OK';
}
$proxy_path = get_proxy_path();
$configuration = CheckConfiguration::run($server);
if (!$configuration) {
throw new \Exception("Configuration is not synced");
}
SaveConfiguration::run($server, $configuration);
$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([ $proxyType = $server->proxyType();
"apt-get update > /dev/null 2>&1 || true", $commands = collect([]);
"command -v lsof >/dev/null || echo '####### Installing lsof.'", $proxy_path = get_proxy_path();
"command -v lsof >/dev/null || apt install -y lsof", $configuration = CheckConfiguration::run($server);
"command -v lsof >/dev/null || command -v fuser >/dev/null || apt install -y psmisc", if (!$configuration) {
"mkdir -p $proxy_path && cd $proxy_path", throw new \Exception("Configuration is not synced");
"echo '####### Creating Docker Compose file.'", }
"echo '####### Pulling docker image.'", SaveConfiguration::run($server, $configuration);
'docker compose pull', $docker_compose_yml_base64 = base64_encode($configuration);
"echo '####### Stopping existing coolify-proxy.'", $server->proxy->last_applied_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value;
"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

@@ -43,19 +43,20 @@ class ResourcesDelete extends Command
$this->deleteDatabase(); $this->deleteDatabase();
} elseif ($resource === 'Service') { } elseif ($resource === 'Service') {
$this->deleteService(); $this->deleteService();
} elseif($resource === 'Server') { } elseif ($resource === 'Server') {
$this->deleteServer(); $this->deleteServer();
} }
} }
private function deleteServer() { private function deleteServer()
{
$servers = Server::all(); $servers = Server::all();
if ($servers->count() === 0) { if ($servers->count() === 0) {
$this->error('There are no applications to delete.'); $this->error('There are no applications to delete.');
return; return;
} }
$serversToDelete = multiselect( $serversToDelete = multiselect(
'What server do you want to delete?', label: 'What server do you want to delete?',
$servers->pluck('id')->sort()->toArray(), options: $servers->pluck('name', 'id')->sortKeys(),
); );
foreach ($serversToDelete as $server) { foreach ($serversToDelete as $server) {
@@ -77,11 +78,12 @@ 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')->sort()->toArray(), $applications->pluck('name', 'id')->sortKeys(),
); );
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); $this->info($toDelete);
$confirmed = confirm("Are you sure you want to delete all selected resources? "); $confirmed = confirm("Are you sure you want to delete all selected resources? ");
if (!$confirmed) { if (!$confirmed) {
@@ -99,11 +101,11 @@ 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')->sort()->toArray(), $databases->pluck('name', 'id')->sortKeys(),
); );
foreach ($databasesToDelete as $database) { foreach ($databasesToDelete as $database) {
$toDelete = $databases->where('name', $database)->first(); $toDelete = $databases->where('id', $database)->first();
$this->info($toDelete); $this->info($toDelete);
$confirmed = confirm("Are you sure you want to delete all selected resources?"); $confirmed = confirm("Are you sure you want to delete all selected resources?");
if (!$confirmed) { if (!$confirmed) {
@@ -111,7 +113,6 @@ class ResourcesDelete extends Command
} }
$toDelete->delete(); $toDelete->delete();
} }
} }
private function deleteService() private function deleteService()
{ {
@@ -122,11 +123,11 @@ 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')->sort()->toArray(), $services->pluck('name', 'id')->sortKeys(),
); );
foreach ($servicesToDelete as $service) { foreach ($servicesToDelete as $service) {
$toDelete = $services->where('name', $service)->first(); $toDelete = $services->where('id', $service)->first();
$this->info($toDelete); $this->info($toDelete);
$confirmed = confirm("Are you sure you want to delete all selected resources?"); $confirmed = confirm("Are you sure you want to delete all selected resources?");
if (!$confirmed) { if (!$confirmed) {

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

@@ -77,6 +77,10 @@ class General extends Component
]; ];
public function updatedApplicationBuildPack(){ public function updatedApplicationBuildPack(){
if ($this->application->build_pack !== 'nixpacks') {
$this->application->settings->is_static = $this->is_static = false;
$this->application->settings->save();
}
$this->submit(); $this->submit();
} }
public function instantSave() public function instantSave()

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

@@ -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,8 @@ 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() { public function checkLocalhostConnection()
{
$uptime = $this->server->validateConnection(); $uptime = $this->server->validateConnection();
if ($uptime) { if ($uptime) {
$this->emit('success', 'Server is reachable.'); $this->emit('success', 'Server is reachable.');
@@ -80,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 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();
@@ -120,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();

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);
$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);
$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

@@ -67,10 +67,13 @@ 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';
public $tries = 1; public $tries = 1;
public function __construct(int $application_deployment_queue_id) public function __construct(int $application_deployment_queue_id)
{ {
@@ -92,13 +95,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,18 +140,43 @@ 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);
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()) {
@@ -184,41 +211,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 +284,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 +300,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 +325,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(
[ [
@@ -333,9 +361,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} }
} }
$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 +455,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 +463,7 @@ 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}"; $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}";
$this->execute_remote_command( $this->execute_remote_command(
[ [
@@ -511,8 +537,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"),
@@ -664,12 +695,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 +793,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 +826,13 @@ 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 {
ray("docker build $this->addHosts --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}");
$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 +858,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 +881,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 +890,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

@@ -27,11 +27,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 +36,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;
@@ -114,7 +117,6 @@ 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...');
if ($this->server->isProxyShouldRun()) { if ($this->server->isProxyShouldRun()) {
StartProxy::run($this->server, false); StartProxy::run($this->server, false);
$this->server->team->notify(new ContainerRestarted('coolify-proxy', $this->server)); $this->server->team->notify(new ContainerRestarted('coolify-proxy', $this->server));

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);
} }

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()

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) {

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

@@ -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

@@ -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.78', 'release' => '4.0.0-beta.89',
// 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.78'; return '4.0.0-beta.89';

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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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,48 @@
@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-green-400 -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>
@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-green-400 -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>
@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-green-400 -top-1 -left-1 badge badge-xs"></div>
@elseif (Str::of(serviceStatus($service))->startsWith('degraded'))
<div class="absolute bg-yellow-400 -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

@@ -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

File diff suppressed because one or more lines are too long

View File

@@ -4,7 +4,7 @@
"version": "3.12.36" "version": "3.12.36"
}, },
"v4": { "v4": {
"version": "4.0.0-beta.78" "version": "4.0.0-beta.89"
} }
} }
} }