Merge pull request #1962 from coollabsio/next

v4.0.0-beta.256
This commit is contained in:
Andras Bacsai 2024-04-12 14:31:49 +02:00 committed by GitHub
commit 3bfac7ef3b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
131 changed files with 3683 additions and 436 deletions

View File

@ -33,12 +33,13 @@ # Donations
<a href="https://cccareers.org/" target="_blank"><img src="./other/logos/ccc-logo.webp" alt="cccareers logo" width="200"/></a> <a href="https://cccareers.org/" target="_blank"><img src="./other/logos/ccc-logo.webp" alt="cccareers logo" width="200"/></a>
## Github Sponsors ($40+) ## Github Sponsors ($40+)
<a href="https://bc.direct"><img width="60px" alt="BC Direct" src="https://github.com/coollabsio/coolify/assets/5845193/a4063c41-95ed-4a32-8814-cd1475572e37"/></a>
<a href="https://typebot.io/?utm_source=coolify.io"><img src="https://pbs.twimg.com/profile_images/1509194008366657543/9I-C7uWT_400x400.jpg" width="60px" alt="typebot"/></a>
<a href="https://americancloud.com/?utm_source=coolify.io"><img src="https://github.com/American-Cloud.png" width="60px" alt="American Cloud"/></a> <a href="https://americancloud.com/?utm_source=coolify.io"><img src="https://github.com/American-Cloud.png" width="60px" alt="American Cloud"/></a>
<a href="https://cryptojobslist.com/?utm_source=coolify.io"><img src="https://github.com/cryptojobslist.png" width="60px" alt="CryptoJobsList" /></a> <a href="https://cryptojobslist.com/?utm_source=coolify.io"><img src="https://github.com/cryptojobslist.png" width="60px" alt="CryptoJobsList" /></a>
<a href="https://typebot.io/?utm_source=coolify.io"><img src="https://pbs.twimg.com/profile_images/1509194008366657543/9I-C7uWT_400x400.jpg" width="60px" alt="typebot"/></a>
<a href="https://bc.direct"><img width="60px" alt="BC Direct" src="https://github.com/coollabsio/coolify/assets/5845193/a4063c41-95ed-4a32-8814-cd1475572e37"/></a>
<a href="https://www.uxwizz.com/?utm_source=coolify.io"><img width="60px" alt="UXWizz" src="https://github.com/UXWizz.png"/></a> <a href="https://www.uxwizz.com/?utm_source=coolify.io"><img width="60px" alt="UXWizz" src="https://github.com/UXWizz.png"/></a>
<a href="https://github.com/automazeio"><img src="https://github.com/automazeio.png" width="60px" alt="Corentin Clichy" /></a> <a href="https://github.com/Flowko"><img src="https://barrad.me/_ipx/f_webp&s_300x300/younes.jpg" width="60px" alt="Younes Barrad" /></a>
<a href="https://github.com/automazeio"><img src="https://github.com/automazeio.png" width="60px" alt="Automaze" /></a>
<a href="https://github.com/corentinclichy"><img src="https://github.com/corentinclichy.png" width="60px" alt="Corentin Clichy" /></a> <a href="https://github.com/corentinclichy"><img src="https://github.com/corentinclichy.png" width="60px" alt="Corentin Clichy" /></a>
<a href="https://github.com/Niki2k1"><img src="https://github.com/Niki2k1.png" width="60px" alt="Niklas Lausch" /></a> <a href="https://github.com/Niki2k1"><img src="https://github.com/Niki2k1.png" width="60px" alt="Niklas Lausch" /></a>
<a href="https://github.com/pixelinfinito"><img src="https://github.com/pixelinfinito.png" width="60px" alt="Pixel Infinito" /></a> <a href="https://github.com/pixelinfinito"><img src="https://github.com/pixelinfinito.png" width="60px" alt="Pixel Infinito" /></a>

View File

@ -0,0 +1,159 @@
<?php
namespace App\Actions\Database;
use App\Models\StandaloneClickhouse;
use Illuminate\Support\Str;
use Symfony\Component\Yaml\Yaml;
use Lorisleiva\Actions\Concerns\AsAction;
class StartClickhouse
{
use AsAction;
public StandaloneClickhouse $database;
public array $commands = [];
public string $configuration_dir;
public function handle(StandaloneClickhouse $database)
{
$this->database = $database;
$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();
$docker_compose = [
'version' => '3.8',
'services' => [
$container_name => [
'image' => $this->database->image,
'container_name' => $container_name,
'environment' => $environment_variables,
'restart' => RESTART_MODE,
'networks' => [
$this->database->destination->network,
],
'ulimits' => [
'nofile' => [
'soft' => 262144,
'hard' => 262144,
],
],
'labels' => [
'coolify.managed' => 'true',
],
'healthcheck' => [
'test' => "clickhouse-client --password {$this->database->clickhouse_admin_password} --query 'SELECT 1'",
'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' => (float) $this->database->limits_cpus,
'cpu_shares' => $this->database->limits_cpu_shares,
]
],
'networks' => [
$this->database->destination->network => [
'external' => true,
'name' => $this->database->destination->network,
'attachable' => true,
]
]
];
if (!is_null($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => "tcp://127.0.0.1:24224",
'fluentd-async' => "true",
'fluentd-sub-second-precision' => "true",
]
];
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
}
if (count($persistent_storages) > 0) {
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
}
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
$readme = generate_readme_file($this->database->name, now());
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo 'Database started.'";
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
}
private function generate_local_persistent_volumes()
{
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
$local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
} else {
$volume_name = $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->real_value");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('CLICKHOUSE_ADMIN_USER'))->isEmpty()) {
$environment_variables->push("CLICKHOUSE_ADMIN_USER={$this->database->clickhouse_admin_user}");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('CLICKHOUSE_ADMIN_PASSWORD'))->isEmpty()) {
$environment_variables->push("CLICKHOUSE_ADMIN_PASSWORD={$this->database->clickhouse_admin_password}");
}
return $environment_variables->all();
}
}

View File

@ -3,6 +3,9 @@
namespace App\Actions\Database; namespace App\Actions\Database;
use App\Models\ServiceDatabase; use App\Models\ServiceDatabase;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
use App\Models\StandaloneMariadb; use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb; use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql; use App\Models\StandaloneMysql;
@ -15,7 +18,7 @@ class StartDatabaseProxy
{ {
use AsAction; use AsAction;
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|ServiceDatabase $database) public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse|ServiceDatabase $database)
{ {
$internalPort = null; $internalPort = null;
$type = $database->getMorphClass(); $type = $database->getMorphClass();
@ -50,6 +53,18 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St
$type = 'App\Models\StandaloneRedis'; $type = 'App\Models\StandaloneRedis';
$containerName = "redis-{$database->service->uuid}"; $containerName = "redis-{$database->service->uuid}";
break; break;
case 'standalone-keydb':
$type = 'App\Models\StandaloneKeydb';
$containerName = "keydb-{$database->service->uuid}";
break;
case 'standalone-dragonfly':
$type = 'App\Models\StandaloneDragonfly';
$containerName = "dragonfly-{$database->service->uuid}";
break;
case 'standalone-clickhouse':
$type = 'App\Models\StandaloneClickhouse';
$containerName = "clickhouse-{$database->service->uuid}";
break;
} }
} }
if ($type === 'App\Models\StandaloneRedis') { if ($type === 'App\Models\StandaloneRedis') {
@ -62,6 +77,12 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St
$internalPort = 3306; $internalPort = 3306;
} else if ($type === 'App\Models\StandaloneMariadb') { } else if ($type === 'App\Models\StandaloneMariadb') {
$internalPort = 3306; $internalPort = 3306;
} else if ($type === 'App\Models\StandaloneKeydb') {
$internalPort = 6379;
} else if ($type === 'App\Models\StandaloneDragonfly') {
$internalPort = 6379;
} else if ($type === 'App\Models\StandaloneClickhouse') {
$internalPort = 9000;
} }
$configuration_dir = database_proxy_dir($database->uuid); $configuration_dir = database_proxy_dir($database->uuid);
$nginxconf = <<<EOF $nginxconf = <<<EOF

View File

@ -0,0 +1,156 @@
<?php
namespace App\Actions\Database;
use App\Models\StandaloneDragonfly;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Symfony\Component\Yaml\Yaml;
use Lorisleiva\Actions\Concerns\AsAction;
class StartDragonfly
{
use AsAction;
public StandaloneDragonfly $database;
public array $commands = [];
public string $configuration_dir;
public function handle(StandaloneDragonfly $database)
{
$this->database = $database;
$startCommand = "dragonfly --requirepass {$this->database->dragonfly_password}";
$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();
$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,
],
'ulimits' => [
'memlock'=> '-1'
],
'labels' => [
'coolify.managed' => 'true',
],
'healthcheck' => [
'test' => "redis-cli -a {$this->database->dragonfly_password} 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' => (float) $this->database->limits_cpus,
'cpu_shares' => $this->database->limits_cpu_shares,
]
],
'networks' => [
$this->database->destination->network => [
'external' => true,
'name' => $this->database->destination->network,
'attachable' => true,
]
]
];
if (!is_null($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => "tcp://127.0.0.1:24224",
'fluentd-async' => "true",
'fluentd-sub-second-precision' => "true",
]
];
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
}
if (count($persistent_storages) > 0) {
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
}
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
$readme = generate_readme_file($this->database->name, now());
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo 'Database started.'";
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
}
private function generate_local_persistent_volumes()
{
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
$local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
} else {
$volume_name = $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->real_value");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
$environment_variables->push("REDIS_PASSWORD={$this->database->dragonfly_password}");
}
return $environment_variables->all();
}
}

View File

@ -0,0 +1,174 @@
<?php
namespace App\Actions\Database;
use App\Models\StandaloneKeydb;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Symfony\Component\Yaml\Yaml;
use Lorisleiva\Actions\Concerns\AsAction;
class StartKeydb
{
use AsAction;
public StandaloneKeydb $database;
public array $commands = [];
public string $configuration_dir;
public function handle(StandaloneKeydb $database)
{
$this->database = $database;
$startCommand = "keydb-server --requirepass {$this->database->keydb_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_keydb();
$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' => "keydb-cli --pass {$this->database->keydb_password} 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' => (float) $this->database->limits_cpus,
'cpu_shares' => $this->database->limits_cpu_shares,
]
],
'networks' => [
$this->database->destination->network => [
'external' => true,
'name' => $this->database->destination->network,
'attachable' => true,
]
]
];
if (!is_null($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => "tcp://127.0.0.1:24224",
'fluentd-async' => "true",
'fluentd-sub-second-precision' => "true",
]
];
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
}
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->keydb_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
'source' => $this->configuration_dir . '/keydb.conf',
'target' => '/etc/keydb/keydb.conf',
'read_only' => true,
];
$docker_compose['services'][$container_name]['command'] = "keydb-server /etc/keydb/keydb.conf --requirepass {$this->database->keydb_password} --appendonly yes";
}
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
$readme = generate_readme_file($this->database->name, now());
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo 'Database started.'";
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
}
private function generate_local_persistent_volumes()
{
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
$local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
} else {
$volume_name = $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->real_value");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
$environment_variables->push("REDIS_PASSWORD={$this->database->keydb_password}");
}
return $environment_variables->all();
}
private function add_custom_keydb()
{
if (is_null($this->database->keydb_conf)) {
return;
}
$filename = 'keydb.conf';
Storage::disk('local')->put("tmp/keydb.conf_{$this->database->uuid}", $this->database->keydb_conf);
$path = Storage::path("tmp/keydb.conf_{$this->database->uuid}");
instant_scp($path, "{$this->configuration_dir}/{$filename}", $this->database->destination->server);
Storage::disk('local')->delete("tmp/keydb.conf_{$this->database->uuid}");
}
}

View File

@ -114,8 +114,12 @@ private function generate_local_persistent_volumes()
{ {
$local_persistent_volumes = []; $local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) { foreach ($this->database->persistentStorages as $persistentStorage) {
$volume_name = $persistentStorage->host_path ?? $persistentStorage->name; if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path; $local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
} else {
$volume_name = $persistentStorage->name;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
} }
return $local_persistent_volumes; return $local_persistent_volumes;
} }

View File

@ -130,8 +130,12 @@ private function generate_local_persistent_volumes()
{ {
$local_persistent_volumes = []; $local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) { foreach ($this->database->persistentStorages as $persistentStorage) {
$volume_name = $persistentStorage->host_path ?? $persistentStorage->name; if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path; $local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
} else {
$volume_name = $persistentStorage->name;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
} }
return $local_persistent_volumes; return $local_persistent_volumes;
} }

View File

@ -114,8 +114,12 @@ private function generate_local_persistent_volumes()
{ {
$local_persistent_volumes = []; $local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) { foreach ($this->database->persistentStorages as $persistentStorage) {
$volume_name = $persistentStorage->host_path ?? $persistentStorage->name; if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path; $local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
} else {
$volume_name = $persistentStorage->name;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
} }
return $local_persistent_volumes; return $local_persistent_volumes;
} }

View File

@ -136,8 +136,12 @@ private function generate_local_persistent_volumes()
{ {
$local_persistent_volumes = []; $local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) { foreach ($this->database->persistentStorages as $persistentStorage) {
$volume_name = $persistentStorage->host_path ?? $persistentStorage->name; if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path; $local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
} else {
$volume_name = $persistentStorage->name;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
} }
return $local_persistent_volumes; return $local_persistent_volumes;
} }

View File

@ -125,8 +125,12 @@ private function generate_local_persistent_volumes()
{ {
$local_persistent_volumes = []; $local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) { foreach ($this->database->persistentStorages as $persistentStorage) {
$volume_name = $persistentStorage->host_path ?? $persistentStorage->name; if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path; $local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
} else {
$volume_name = $persistentStorage->name;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
} }
return $local_persistent_volumes; return $local_persistent_volumes;
} }

View File

@ -2,7 +2,9 @@
namespace App\Actions\Database; namespace App\Actions\Database;
use App\Events\DatabaseStatusChanged; use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
use App\Models\StandaloneMariadb; use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb; use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql; use App\Models\StandaloneMysql;
@ -14,7 +16,7 @@ class StopDatabase
{ {
use AsAction; use AsAction;
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $database) public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database)
{ {
$server = $database->destination->server; $server = $database->destination->server;
if (!$server->isFunctional()) { if (!$server->isFunctional()) {

View File

@ -3,6 +3,9 @@
namespace App\Actions\Database; namespace App\Actions\Database;
use App\Models\ServiceDatabase; use App\Models\ServiceDatabase;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
use App\Models\StandaloneMariadb; use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb; use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql; use App\Models\StandaloneMysql;
@ -14,7 +17,7 @@ class StopDatabaseProxy
{ {
use AsAction; use AsAction;
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|ServiceDatabase $database) public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|ServiceDatabase|StandaloneDragonfly|StandaloneClickhouse $database)
{ {
$server = data_get($database, 'destination.server'); $server = data_get($database, 'destination.server');
$uuid = $database->uuid; $uuid = $database->uuid;

View File

@ -7,6 +7,9 @@
use App\Models\Service; use App\Models\Service;
use App\Models\ServiceApplication; use App\Models\ServiceApplication;
use App\Models\ServiceDatabase; use App\Models\ServiceDatabase;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
use App\Models\StandaloneMariadb; use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb; use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql; use App\Models\StandaloneMysql;
@ -55,6 +58,33 @@ private function cleanup_stucked_resources()
} catch (\Throwable $e) { } catch (\Throwable $e) {
echo "Error in cleaning stuck redis: {$e->getMessage()}\n"; echo "Error in cleaning stuck redis: {$e->getMessage()}\n";
} }
try {
$keydbs = StandaloneKeydb::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($keydbs as $keydb) {
echo "Deleting stuck keydb: {$keydb->name}\n";
$redis->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck keydb: {$e->getMessage()}\n";
}
try {
$dragonflies = StandaloneDragonfly::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($dragonflies as $dragonfly) {
echo "Deleting stuck dragonfly: {$dragonfly->name}\n";
$redis->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck dragonfly: {$e->getMessage()}\n";
}
try {
$clickhouses = StandaloneClickhouse::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($clickhouses as $clickhouse) {
echo "Deleting stuck clickhouse: {$clickhouse->name}\n";
$redis->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck clickhouse: {$e->getMessage()}\n";
}
try { try {
$mongodbs = StandaloneMongodb::withTrashed()->whereNotNull('deleted_at')->get(); $mongodbs = StandaloneMongodb::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($mongodbs as $mongodb) { foreach ($mongodbs as $mongodb) {

View File

@ -33,6 +33,7 @@ protected function schedule(Schedule $schedule): void
$this->check_scheduled_backups($schedule); $this->check_scheduled_backups($schedule);
$this->pull_helper_image($schedule); $this->pull_helper_image($schedule);
$this->check_scheduled_tasks($schedule); $this->check_scheduled_tasks($schedule);
$schedule->command('uploads:clear')->everyTwoMinutes();
} else { } else {
// Instance Jobs // Instance Jobs
$schedule->command('horizon:snapshot')->everyFiveMinutes(); $schedule->command('horizon:snapshot')->everyFiveMinutes();
@ -49,6 +50,7 @@ protected function schedule(Schedule $schedule): void
$this->check_scheduled_tasks($schedule); $this->check_scheduled_tasks($schedule);
$schedule->command('cleanup:database --yes')->daily(); $schedule->command('cleanup:database --yes')->daily();
$schedule->command('uploads:clear')->everyTwoMinutes();
} }
} }
private function pull_helper_image($schedule) private function pull_helper_image($schedule)

View File

@ -2,6 +2,9 @@
namespace App\Http\Controllers\Api; namespace App\Http\Controllers\Api;
use App\Actions\Database\StartClickhouse;
use App\Actions\Database\StartDragonfly;
use App\Actions\Database\StartKeydb;
use App\Actions\Database\StartMariadb; use App\Actions\Database\StartMariadb;
use App\Actions\Database\StartMongodb; use App\Actions\Database\StartMongodb;
use App\Actions\Database\StartMysql; use App\Actions\Database\StartMysql;
@ -157,6 +160,24 @@ public function deploy_resource($resource, bool $force = false): array
'started_at' => now(), 'started_at' => now(),
]); ]);
$message = "Database {$resource->name} started."; $message = "Database {$resource->name} started.";
} else if ($type === 'App\Models\StandaloneKeydb') {
StartKeydb::run($resource);
$resource->update([
'started_at' => now(),
]);
$message = "Database {$resource->name} started.";
} else if ($type === 'App\Models\StandaloneDragonfly') {
StartDragonfly::run($resource);
$resource->update([
'started_at' => now(),
]);
$message = "Database {$resource->name} started.";
} else if ($type === 'App\Models\StandaloneClickhouse') {
StartClickhouse::run($resource);
$resource->update([
'started_at' => now(),
]);
$message = "Database {$resource->name} started.";
} else if ($type === 'App\Models\StandaloneMongodb') { } else if ($type === 'App\Models\StandaloneMongodb') {
StartMongodb::run($resource); StartMongodb::run($resource);
$resource->update([ $resource->update([

View File

@ -0,0 +1,83 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Http\JsonResponse;
use Pion\Laravel\ChunkUpload\Exceptions\UploadFailedException;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Pion\Laravel\ChunkUpload\Exceptions\UploadMissingFileException;
use Pion\Laravel\ChunkUpload\Handler\AbstractHandler;
use Pion\Laravel\ChunkUpload\Handler\HandlerFactory;
use Pion\Laravel\ChunkUpload\Receiver\FileReceiver;
class UploadController extends BaseController
{
public function upload(Request $request)
{
$resource = getResourceByUuid(request()->route('databaseUuid'), data_get(auth()->user()->currentTeam(), 'id'));
if (is_null($resource)) {
return response()->json(['error' => 'You do not have permission for this database'], 500);
}
$receiver = new FileReceiver("file", $request, HandlerFactory::classFromRequest($request));
if ($receiver->isUploaded() === false) {
throw new UploadMissingFileException();
}
$save = $receiver->receive();
if ($save->isFinished()) {
return $this->saveFile($save->getFile(), $resource);
}
$handler = $save->handler();
return response()->json([
"done" => $handler->getPercentageDone(),
'status' => true
]);
}
// protected function saveFileToS3($file)
// {
// $fileName = $this->createFilename($file);
// $disk = Storage::disk('s3');
// // It's better to use streaming Streaming (laravel 5.4+)
// $disk->putFileAs('photos', $file, $fileName);
// // for older laravel
// // $disk->put($fileName, file_get_contents($file), 'public');
// $mime = str_replace('/', '-', $file->getMimeType());
// // We need to delete the file when uploaded to s3
// unlink($file->getPathname());
// return response()->json([
// 'path' => $disk->url($fileName),
// 'name' => $fileName,
// 'mime_type' => $mime
// ]);
// }
protected function saveFile(UploadedFile $file, $resource)
{
$mime = str_replace('/', '-', $file->getMimeType());
$filePath = "upload/{$resource->uuid}";
$finalPath = storage_path("app/" . $filePath);
$file->move($finalPath, 'restore');
return response()->json([
'mime_type' => $mime
]);
}
protected function createFilename(UploadedFile $file)
{
$extension = $file->getClientOriginalExtension();
$filename = str_replace("." . $extension, "", $file->getClientOriginalName()); // Filename without extension
$filename .= "_" . md5(time()) . "." . $extension;
return $filename;
}
}

View File

@ -83,6 +83,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private $env_nixpacks_args; private $env_nixpacks_args;
private $docker_compose; private $docker_compose;
private $docker_compose_base64; private $docker_compose_base64;
private ?string $env_filename = null;
private ?string $nixpacks_plan = null; private ?string $nixpacks_plan = null;
private ?string $nixpacks_type = null; private ?string $nixpacks_type = null;
private string $dockerfile_location = '/Dockerfile'; private string $dockerfile_location = '/Dockerfile';
@ -239,50 +240,7 @@ public function handle(): void
$this->build_server = $this->server; $this->build_server = $this->server;
$this->original_server = $this->server; $this->original_server = $this->server;
} }
if ($this->restart_only && $this->application->build_pack !== 'dockerimage' && $this->application->build_pack !== 'dockerfile') { $this->decide_what_to_do();
$this->just_restart();
if ($this->server->isProxyShouldRun()) {
dispatch(new ContainerStatusJob($this->server));
}
$this->next(ApplicationDeploymentStatus::FINISHED->value);
$this->application->isConfigurationChanged(false);
$this->run_post_deployment_command();
return;
} else if ($this->pull_request_id !== 0) {
$this->deploy_pull_request();
} else if ($this->application->dockerfile) {
$this->deploy_simple_dockerfile();
} else if ($this->application->build_pack === 'dockercompose') {
$this->deploy_docker_compose_buildpack();
} else if ($this->application->build_pack === 'dockerimage') {
$this->deploy_dockerimage_buildpack();
} else if ($this->application->build_pack === 'dockerfile') {
$this->deploy_dockerfile_buildpack();
} else if ($this->application->build_pack === 'static') {
$this->deploy_static_buildpack();
} else {
$this->deploy_nixpacks_buildpack();
}
if ($this->server->isProxyShouldRun()) {
dispatch(new ContainerStatusJob($this->server));
}
// Otherwise built image needs to be pushed before from the build server.
// ray($this->use_build_server);
// if (!$this->use_build_server) {
// if ($this->application->additional_servers->count() > 0) {
// $this->push_to_docker_registry(forceFail: true);
// } else {
// $this->push_to_docker_registry();
// }
// }
$this->next(ApplicationDeploymentStatus::FINISHED->value);
if ($this->pull_request_id !== 0) {
if ($this->application->is_github_based()) {
ApplicationPullRequestUpdateJob::dispatch(application: $this->application, preview: $this->preview, deployment_uuid: $this->deployment_uuid, status: ProcessStatus::FINISHED);
}
}
$this->run_post_deployment_command();
$this->application->isConfigurationChanged(true);
} catch (Exception $e) { } catch (Exception $e) {
if ($this->pull_request_id !== 0 && $this->application->is_github_based()) { if ($this->pull_request_id !== 0 && $this->application->is_github_based()) {
ApplicationPullRequestUpdateJob::dispatch(application: $this->application, preview: $this->preview, deployment_uuid: $this->deployment_uuid, status: ProcessStatus::ERROR); ApplicationPullRequestUpdateJob::dispatch(application: $this->application, preview: $this->preview, deployment_uuid: $this->deployment_uuid, status: ProcessStatus::ERROR);
@ -317,6 +275,43 @@ public function handle(): void
ApplicationStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id')); ApplicationStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id'));
} }
} }
private function decide_what_to_do()
{
if ($this->restart_only && $this->application->build_pack !== 'dockerimage' && $this->application->build_pack !== 'dockerfile') {
$this->just_restart();
return;
} else if ($this->pull_request_id !== 0) {
$this->deploy_pull_request();
} else if ($this->application->dockerfile) {
$this->deploy_simple_dockerfile();
} else if ($this->application->build_pack === 'dockercompose') {
$this->deploy_docker_compose_buildpack();
} else if ($this->application->build_pack === 'dockerimage') {
$this->deploy_dockerimage_buildpack();
} else if ($this->application->build_pack === 'dockerfile') {
$this->deploy_dockerfile_buildpack();
} else if ($this->application->build_pack === 'static') {
$this->deploy_static_buildpack();
} else {
$this->deploy_nixpacks_buildpack();
}
$this->post_deployment();
}
private function post_deployment()
{
if ($this->server->isProxyShouldRun()) {
dispatch(new ContainerStatusJob($this->server));
}
$this->next(ApplicationDeploymentStatus::FINISHED->value);
if ($this->pull_request_id !== 0) {
if ($this->application->is_github_based()) {
ApplicationPullRequestUpdateJob::dispatch(application: $this->application, preview: $this->preview, deployment_uuid: $this->deployment_uuid, status: ProcessStatus::FINISHED);
}
}
$this->run_post_deployment_command();
$this->application->isConfigurationChanged(true);
}
private function deploy_simple_dockerfile() private function deploy_simple_dockerfile()
{ {
if ($this->use_build_server) { if ($this->use_build_server) {
@ -336,7 +331,6 @@ private function deploy_simple_dockerfile()
// if (!$this->force_rebuild) { // if (!$this->force_rebuild) {
// $this->check_image_locally_or_remotely(); // $this->check_image_locally_or_remotely();
// if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) { // if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
// $this->create_workdir();
// $this->application_deployment_queue->addLogEntry("No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped."); // $this->application_deployment_queue->addLogEntry("No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.");
// $this->generate_compose_file(); // $this->generate_compose_file();
// $this->push_to_docker_registry(); // $this->push_to_docker_registry();
@ -477,7 +471,6 @@ private function deploy_dockerfile_buildpack()
if (!$this->force_rebuild) { if (!$this->force_rebuild) {
$this->check_image_locally_or_remotely(); $this->check_image_locally_or_remotely();
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) { if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
$this->create_workdir();
$this->application_deployment_queue->addLogEntry("No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped."); $this->application_deployment_queue->addLogEntry("No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.");
$this->generate_compose_file(); $this->generate_compose_file();
$this->push_to_docker_registry(); $this->push_to_docker_registry();
@ -506,7 +499,6 @@ private function deploy_nixpacks_buildpack()
if (!$this->force_rebuild) { if (!$this->force_rebuild) {
$this->check_image_locally_or_remotely(); $this->check_image_locally_or_remotely();
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) { if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
$this->create_workdir();
$this->application_deployment_queue->addLogEntry("No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped."); $this->application_deployment_queue->addLogEntry("No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.");
$this->generate_compose_file(); $this->generate_compose_file();
ray('pushing to docker registry'); ray('pushing to docker registry');
@ -540,7 +532,6 @@ private function deploy_static_buildpack()
if (!$this->force_rebuild) { if (!$this->force_rebuild) {
$this->check_image_locally_or_remotely(); $this->check_image_locally_or_remotely();
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) { if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
$this->create_workdir();
$this->application_deployment_queue->addLogEntry("No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped."); $this->application_deployment_queue->addLogEntry("No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.");
$this->generate_compose_file(); $this->generate_compose_file();
$this->push_to_docker_registry(); $this->push_to_docker_registry();
@ -683,7 +674,7 @@ private function generate_image_names()
} }
private function just_restart() private function just_restart()
{ {
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch} to {$this->server->name}."); $this->application_deployment_queue->addLogEntry("Restarting {$this->customRepository}:{$this->application->git_branch} on {$this->server->name}.");
$this->prepare_builder_image(); $this->prepare_builder_image();
$this->check_git_if_build_needed(); $this->check_git_if_build_needed();
$this->set_base_dir(); $this->set_base_dir();
@ -691,12 +682,14 @@ private function just_restart()
$this->check_image_locally_or_remotely(); $this->check_image_locally_or_remotely();
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty()) { if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty()) {
$this->application_deployment_queue->addLogEntry("Image found ({$this->production_image_name}) with the same Git Commit SHA. Restarting container."); $this->application_deployment_queue->addLogEntry("Image found ({$this->production_image_name}) with the same Git Commit SHA. Restarting container.");
$this->create_workdir();
$this->generate_compose_file(); $this->generate_compose_file();
$this->rolling_update(); $this->rolling_update();
return; $this->post_deployment();
} else {
$this->application_deployment_queue->addLogEntry("Image not found ({$this->production_image_name}). Redeploying the application.");
$this->restart_only = false;
$this->decide_what_to_do();
} }
throw new RuntimeException('Cannot find image anywhere. Please redeploy the application.');
} }
private function check_image_locally_or_remotely() private function check_image_locally_or_remotely()
{ {
@ -716,19 +709,35 @@ private function save_environment_variables()
{ {
$envs = collect([]); $envs = collect([]);
if ($this->pull_request_id !== 0) { if ($this->pull_request_id !== 0) {
$this->env_filename = ".env-pr-$this->pull_request_id";
foreach ($this->application->environment_variables_preview as $env) { foreach ($this->application->environment_variables_preview as $env) {
$envs->push($env->key . '=' . $env->real_value); $envs->push($env->key . '=' . $env->real_value);
} }
} else { } else {
$this->env_filename = ".env";
foreach ($this->application->environment_variables as $env) { foreach ($this->application->environment_variables as $env) {
$envs->push($env->key . '=' . $env->real_value); $envs->push($env->key . '=' . $env->real_value);
} }
} }
if ($envs->isEmpty()) {
$this->env_filename = null;
$this->execute_remote_command(
[
"command" => "rm -f $this->configuration_dir/{$this->env_filename}",
"hidden" => true,
"ignore_errors" => true
]
);
return;
}
$envs_base64 = base64_encode($envs->implode("\n")); $envs_base64 = base64_encode($envs->implode("\n"));
$this->execute_remote_command( $this->execute_remote_command(
[ [
executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d > $this->workdir/.env") executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d > $this->workdir/.env")
], ],
[
"echo '$envs_base64' | base64 -d > $this->configuration_dir/{$this->env_filename}"
]
); );
} }
@ -868,6 +877,9 @@ private function create_workdir()
[ [
"command" => executeInDocker($this->deployment_uuid, "mkdir -p {$this->workdir}") "command" => executeInDocker($this->deployment_uuid, "mkdir -p {$this->workdir}")
], ],
[
"command" => "mkdir -p {$this->configuration_dir}"
],
); );
} }
private function prepare_builder_image() private function prepare_builder_image()
@ -890,6 +902,11 @@ private function prepare_builder_image()
} }
$this->application_deployment_queue->addLogEntry("Preparing container with helper image: $helperImage."); $this->application_deployment_queue->addLogEntry("Preparing container with helper image: $helperImage.");
$this->execute_remote_command( $this->execute_remote_command(
[
"command" => "docker rm -f {$this->deployment_uuid}",
"ignore_errors" => true,
"hidden" => true
],
[ [
$runCommand, $runCommand,
"hidden" => true, "hidden" => true,
@ -993,6 +1010,7 @@ private function clone_repository()
$importCommands, "hidden" => true $importCommands, "hidden" => true
] ]
); );
$this->create_workdir();
} }
private function generate_git_import_commands() private function generate_git_import_commands()
@ -1098,6 +1116,7 @@ private function generate_env_variables()
private function generate_compose_file() private function generate_compose_file()
{ {
$this->create_workdir();
$ports = $this->application->settings->is_static ? [80] : $this->application->ports_exposes_array; $ports = $this->application->settings->is_static ? [80] : $this->application->ports_exposes_array;
$onlyPort = null; $onlyPort = null;
if (count($ports) > 0) { if (count($ports) > 0) {
@ -1181,6 +1200,11 @@ private function generate_compose_file()
] ]
] ]
]; ];
if ($this->env_filename) {
$docker_compose['services'][$this->container_name]['env_file'] = [
$this->env_filename
];
}
if (!$this->custom_healthcheck_found) { if (!$this->custom_healthcheck_found) {
$docker_compose['services'][$this->container_name]['healthcheck'] = [ $docker_compose['services'][$this->container_name]['healthcheck'] = [
'test' => [ 'test' => [
@ -1334,13 +1358,18 @@ private function generate_compose_file()
$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]);
$this->save_environment_variables();
} }
private function generate_local_persistent_volumes() private function generate_local_persistent_volumes()
{ {
$local_persistent_volumes = []; $local_persistent_volumes = [];
foreach ($this->application->persistentStorages as $persistentStorage) { foreach ($this->application->persistentStorages as $persistentStorage) {
$volume_name = $persistentStorage->host_path ?? $persistentStorage->name; if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
$volume_name = $persistentStorage->host_path;
} else {
$volume_name = $persistentStorage->name;
}
if ($this->pull_request_id !== 0) { if ($this->pull_request_id !== 0) {
$volume_name = $volume_name . '-pr-' . $this->pull_request_id; $volume_name = $volume_name . '-pr-' . $this->pull_request_id;
} }

View File

@ -8,6 +8,9 @@
use App\Actions\Service\StopService; use App\Actions\Service\StopService;
use App\Models\Application; use App\Models\Application;
use App\Models\Service; use App\Models\Service;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
use App\Models\StandaloneMariadb; use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb; use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql; use App\Models\StandaloneMysql;
@ -25,7 +28,7 @@ class DeleteResourceJob implements ShouldQueue, ShouldBeEncrypted
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $resource) public function __construct(public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource, public bool $deleteConfigurations = false)
{ {
} }
@ -42,6 +45,9 @@ public function handle()
case 'standalone-mongodb': case 'standalone-mongodb':
case 'standalone-mysql': case 'standalone-mysql':
case 'standalone-mariadb': case 'standalone-mariadb':
case 'standalone-keydb':
case 'standalone-dragonfly':
case 'standalone-clickhouse':
StopDatabase::run($this->resource); StopDatabase::run($this->resource);
break; break;
case 'service': case 'service':
@ -49,6 +55,9 @@ public function handle()
DeleteService::run($this->resource); DeleteService::run($this->resource);
break; break;
} }
if ($this->deleteConfigurations) {
$this->resource?->delete_configurations();
}
} catch (\Throwable $e) { } catch (\Throwable $e) {
ray($e->getMessage()); ray($e->getMessage());
send_internal_notification('ContainerStoppingJob failed with: ' . $e->getMessage()); send_internal_notification('ContainerStoppingJob failed with: ' . $e->getMessage());

View File

@ -24,7 +24,7 @@ public function mount()
} }
public function render() public function render()
{ {
return view('livewire.force-password-reset')->layout('layouts.simple'); return view('livewire.force-password-reset');
} }
public function submit() public function submit()
{ {

View File

@ -24,7 +24,7 @@ class General extends Component
public $customLabels; public $customLabels;
public bool $labelsChanged = false; public bool $labelsChanged = false;
public bool $isConfigurationChanged = false; public bool $initLoadingCompose = false;
public ?string $initialDockerComposeLocation = null; public ?string $initialDockerComposeLocation = null;
public ?string $initialDockerComposePrLocation = null; public ?string $initialDockerComposePrLocation = null;
@ -123,19 +123,22 @@ public function mount()
$this->application->settings->save(); $this->application->settings->save();
} }
$this->parsedServiceDomains = $this->application->docker_compose_domains ? json_decode($this->application->docker_compose_domains, true) : []; $this->parsedServiceDomains = $this->application->docker_compose_domains ? json_decode($this->application->docker_compose_domains, true) : [];
$this->ports_exposes = $this->application->ports_exposes; $this->ports_exposes = $this->application->ports_exposes;
if (str($this->application->status)->startsWith('running') && is_null($this->application->config_hash)) { $this->customLabels = $this->application->parseContainerLabels();
$this->application->isConfigurationChanged(true);
}
$this->isConfigurationChanged = $this->application->isConfigurationChanged();
$this->customLabels = $this->application->parseContainerLabels();
if (!$this->customLabels && $this->application->destination->server->proxyType() !== 'NONE') { if (!$this->customLabels && $this->application->destination->server->proxyType() !== 'NONE') {
$this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n"); $this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n");
$this->application->custom_labels = base64_encode($this->customLabels); $this->application->custom_labels = base64_encode($this->customLabels);
$this->application->save(); $this->application->save();
} }
$this->initialDockerComposeLocation = $this->application->docker_compose_location; $this->initialDockerComposeLocation = $this->application->docker_compose_location;
if ($this->application->build_pack === 'dockercompose' && !$this->application->docker_compose_raw) {
$this->initLoadingCompose = true;
$this->dispatch('info', 'Loading docker compose file...');
}
if (str($this->application->status)->startsWith('running') && is_null($this->application->config_hash)) {
$this->dispatch('configurationChanged');
}
} }
public function instantSave() public function instantSave()
{ {
@ -154,11 +157,15 @@ public function loadComposeFile($isInit = false)
} }
['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation, 'initialDockerComposePrLocation' => $this->initialDockerComposePrLocation] = $this->application->loadComposeFile($isInit); ['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation, 'initialDockerComposePrLocation' => $this->initialDockerComposePrLocation] = $this->application->loadComposeFile($isInit);
$this->dispatch('success', 'Docker compose file loaded.'); $this->dispatch('success', 'Docker compose file loaded.');
$this->dispatch('compose_loaded');
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->application->docker_compose_location = $this->initialDockerComposeLocation; $this->application->docker_compose_location = $this->initialDockerComposeLocation;
$this->application->docker_compose_pr_location = $this->initialDockerComposePrLocation; $this->application->docker_compose_pr_location = $this->initialDockerComposePrLocation;
$this->application->save(); $this->application->save();
return handleError($e, $this); return handleError($e, $this);
} finally {
$this->initLoadingCompose = false;
} }
} }
public function generateDomain(string $serviceName) public function generateDomain(string $serviceName)
@ -307,7 +314,7 @@ public function submit($showToaster = true)
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} finally { } finally {
$this->isConfigurationChanged = $this->application->isConfigurationChanged(); $this->dispatch('configurationChanged');
} }
} }
} }

View File

@ -22,6 +22,7 @@ public function getListeners()
$teamId = auth()->user()->currentTeam()->id; $teamId = auth()->user()->currentTeam()->id;
return [ return [
"echo-private:team.{$teamId},ApplicationStatusChanged" => 'check_status', "echo-private:team.{$teamId},ApplicationStatusChanged" => 'check_status',
"compose_loaded" => '$refresh',
]; ];
} }
public function mount() public function mount()
@ -38,6 +39,7 @@ public function check_status($showNotification = false)
} }
if ($showNotification) $this->dispatch('success', "Success", "Application status updated."); if ($showNotification) $this->dispatch('success', "Success", "Application status updated.");
$this->dispatch('configurationChanged');
} }
public function force_deploy_without_cache() public function force_deploy_without_cache()

View File

@ -8,7 +8,8 @@ class Index extends Component
{ {
public $database; public $database;
public $s3s; public $s3s;
public function mount() { public function mount()
{
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first(); $project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) { if (!$project) {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
@ -21,8 +22,13 @@ public function mount() {
if (!$database) { if (!$database) {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
// No backups for redis // No backups
if ($database->getMorphClass() === 'App\Models\StandaloneRedis') { if (
$database->getMorphClass() === 'App\Models\StandaloneRedis' ||
$database->getMorphClass() === 'App\Models\StandaloneKeydb' ||
$database->getMorphClass() === 'App\Models\StandaloneDragonfly'||
$database->getMorphClass() === 'App\Models\StandaloneClickhouse'
) {
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,

View File

@ -4,6 +4,7 @@
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Livewire\Component; use Livewire\Component;
use Symfony\Component\HttpFoundation\StreamedResponse;
class BackupExecutions extends Component class BackupExecutions extends Component
{ {
@ -36,32 +37,9 @@ public function deleteBackup($exeuctionId)
$this->dispatch('success', 'Backup deleted.'); $this->dispatch('success', 'Backup deleted.');
$this->refreshBackupExecutions(); $this->refreshBackupExecutions();
} }
public function download($exeuctionId) public function download_file($exeuctionId)
{ {
try { return redirect()->route('download.backup', $exeuctionId);
$execution = $this->backup->executions()->where('id', $exeuctionId)->first();
if (is_null($execution)) {
$this->dispatch('error', 'Backup execution not found.');
return;
}
$filename = data_get($execution, 'filename');
if ($execution->scheduledDatabaseBackup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
$server = $execution->scheduledDatabaseBackup->database->service->destination->server;
} else {
$server = $execution->scheduledDatabaseBackup->database->destination->server;
}
$privateKeyLocation = savePrivateKeyToFs($server);
$disk = Storage::build([
'driver' => 'sftp',
'host' => $server->ip,
'port' => $server->port,
'username' => $server->user,
'privateKey' => $privateKeyLocation,
]);
return $disk->download($filename);
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function refreshBackupExecutions(): void public function refreshBackupExecutions(): void
{ {

View File

@ -0,0 +1,115 @@
<?php
namespace App\Livewire\Project\Database\Clickhouse;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Models\StandaloneClickhouse;
use Exception;
use Livewire\Component;
class General extends Component
{
public StandaloneClickhouse $database;
public ?string $db_url = null;
public ?string $db_url_public = null;
protected $listeners = ['refresh'];
protected $rules = [
'database.name' => 'required',
'database.description' => 'nullable',
'database.clickhouse_admin_user' => 'required',
'database.clickhouse_admin_password' => 'required',
'database.image' => 'required',
'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
];
protected $validationAttributes = [
'database.name' => 'Name',
'database.description' => 'Description',
'database.clickhouse_admin_user' => 'Postgres User',
'database.clickhouse_admin_password' => 'Postgres Password',
'database.image' => 'Image',
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
];
public function mount()
{
$this->db_url = $this->database->get_db_url(true);
if ($this->database->is_public) {
$this->db_url_public = $this->database->get_db_url();
}
}
public function instantSaveAdvanced() {
try {
if (!$this->database->destination->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
return;
}
$this->database->save();
$this->dispatch('success', 'Database updated.');
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function instantSave()
{
try {
if ($this->database->is_public && !$this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
if (!str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
return;
}
StartDatabaseProxy::run($this->database);
$this->db_url_public = $this->database->get_db_url();
$this->dispatch('success', 'Database is now publicly accessible.');
} else {
StopDatabaseProxy::run($this->database);
$this->db_url_public = null;
$this->dispatch('success', 'Database is no longer publicly accessible.');
}
$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 submit()
{
try {
if (str($this->database->public_port)->isEmpty()) {
$this->database->public_port = null;
}
$this->validate();
$this->database->save();
$this->dispatch('success', 'Database updated.');
} catch (Exception $e) {
return handleError($e, $this);
} finally {
if (is_null($this->database->config_hash)) {
$this->database->isConfigurationChanged(true);
} else {
$this->dispatch('configurationChanged');
}
}
}
}

View File

@ -7,7 +7,8 @@
class Configuration extends Component class Configuration extends Component
{ {
public $database; public $database;
public function mount() { public function mount()
{
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first(); $project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) { if (!$project) {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
@ -21,6 +22,10 @@ public function mount() {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
$this->database = $database; $this->database = $database;
if (str($this->database->status)->startsWith('running') && is_null($this->database->config_hash)) {
$this->database->isConfigurationChanged(true);
$this->dispatch('configurationChanged');
}
} }
public function render() public function render()
{ {

View File

@ -0,0 +1,113 @@
<?php
namespace App\Livewire\Project\Database\Dragonfly;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Models\StandaloneDragonfly;
use Exception;
use Livewire\Component;
class General extends Component
{
protected $listeners = ['refresh'];
public StandaloneDragonfly $database;
public ?string $db_url = null;
public ?string $db_url_public = null;
protected $rules = [
'database.name' => 'required',
'database.description' => 'nullable',
'database.dragonfly_password' => 'required',
'database.image' => 'required',
'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
];
protected $validationAttributes = [
'database.name' => 'Name',
'database.description' => 'Description',
'database.dragonfly_password' => 'Redis Password',
'database.image' => 'Image',
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
];
public function mount()
{
$this->db_url = $this->database->get_db_url(true);
if ($this->database->is_public) {
$this->db_url_public = $this->database->get_db_url();
}
}
public function instantSaveAdvanced() {
try {
if (!$this->database->destination->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
return;
}
$this->database->save();
$this->dispatch('success', 'Database updated.');
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function submit()
{
try {
$this->validate();
$this->database->save();
$this->dispatch('success', 'Database updated.');
} catch (Exception $e) {
return handleError($e, $this);
} finally {
if (is_null($this->database->config_hash)) {
$this->database->isConfigurationChanged(true);
} else {
$this->dispatch('configurationChanged');
}
}
}
public function instantSave()
{
try {
if ($this->database->is_public && !$this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
if (!str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
return;
}
StartDatabaseProxy::run($this->database);
$this->db_url_public = $this->database->get_db_url();
$this->dispatch('success', 'Database is now publicly accessible.');
} else {
StopDatabaseProxy::run($this->database);
$this->db_url_public = null;
$this->dispatch('success', 'Database is no longer publicly accessible.');
}
$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 render()
{
return view('livewire.project.database.dragonfly.general');
}
}

View File

@ -2,13 +2,15 @@
namespace App\Livewire\Project\Database; namespace App\Livewire\Project\Database;
use App\Actions\Database\StartClickhouse;
use App\Actions\Database\StartDragonfly;
use App\Actions\Database\StartKeydb;
use App\Actions\Database\StartMariadb; use App\Actions\Database\StartMariadb;
use App\Actions\Database\StartMongodb; use App\Actions\Database\StartMongodb;
use App\Actions\Database\StartMysql; use App\Actions\Database\StartMysql;
use App\Actions\Database\StartPostgresql; use App\Actions\Database\StartPostgresql;
use App\Actions\Database\StartRedis; use App\Actions\Database\StartRedis;
use App\Actions\Database\StopDatabase; use App\Actions\Database\StopDatabase;
use App\Events\DatabaseStatusChanged;
use App\Jobs\ContainerStatusJob; use App\Jobs\ContainerStatusJob;
use Livewire\Component; use Livewire\Component;
@ -32,6 +34,12 @@ public function activityFinished()
]); ]);
$this->dispatch('refresh'); $this->dispatch('refresh');
$this->check_status(); $this->check_status();
if (is_null($this->database->config_hash) || $this->database->isConfigurationChanged()) {
$this->database->isConfigurationChanged(true);
$this->dispatch('configurationChanged');
} else {
$this->dispatch('configurationChanged');
}
} }
public function check_status($showNotification = false) public function check_status($showNotification = false)
@ -71,6 +79,15 @@ public function start()
} else if ($this->database->type() === 'standalone-mariadb') { } else if ($this->database->type() === 'standalone-mariadb') {
$activity = StartMariadb::run($this->database); $activity = StartMariadb::run($this->database);
$this->dispatch('activityMonitor', $activity->id); $this->dispatch('activityMonitor', $activity->id);
} else if ($this->database->type() === 'standalone-keydb') {
$activity = StartKeydb::run($this->database);
$this->dispatch('activityMonitor', $activity->id);
} else if ($this->database->type() === 'standalone-dragonfly') {
$activity = StartDragonfly::run($this->database);
$this->dispatch('activityMonitor', $activity->id);
} else if ($this->database->type() === 'standalone-clickhouse') {
$activity = StartClickhouse::run($this->database);
$this->dispatch('activityMonitor', $activity->id);
} }
} }
} }

View File

@ -2,29 +2,25 @@
namespace App\Livewire\Project\Database; namespace App\Livewire\Project\Database;
use Exception;
use Livewire\Component; use Livewire\Component;
use Livewire\WithFileUploads;
use App\Models\Server; use App\Models\Server;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
class Import extends Component class Import extends Component
{ {
use WithFileUploads; public bool $unsupported = false;
public $file;
public $resource; public $resource;
public $parameters; public $parameters;
public $containers; public $containers;
public bool $validated = true;
public bool $scpInProgress = false; public bool $scpInProgress = false;
public bool $importRunning = false; public bool $importRunning = false;
public string $validationMsg = '';
public ?string $filename = null;
public ?string $filesize = null;
public bool $isUploading = false;
public int $progress = 0;
public bool $error = false;
public Server $server; public Server $server;
public string $container; public string $container;
public array $importCommands = []; public array $importCommands = [];
@ -51,22 +47,9 @@ public function getContainers()
if (!data_get($this->parameters, 'database_uuid')) { if (!data_get($this->parameters, 'database_uuid')) {
abort(404); abort(404);
} }
$resource = getResourceByUuid($this->parameters['database_uuid'], data_get(auth()->user()->currentTeam(), 'id'));
$resource = StandalonePostgresql::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) { if (is_null($resource)) {
$resource = StandaloneRedis::where('uuid', $this->parameters['database_uuid'])->first(); abort(404);
if (is_null($resource)) {
$resource = StandaloneMongodb::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
$resource = StandaloneMysql::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
$resource = StandaloneMariadb::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
abort(404);
}
}
}
}
} }
$this->resource = $resource; $this->resource = $resource;
$this->server = $this->resource->destination->server; $this->server = $this->resource->destination->server;
@ -75,38 +58,34 @@ public function getContainers()
$this->containers->push($this->container); $this->containers->push($this->container);
} }
if ($this->containers->count() > 1) {
$this->validated = false;
$this->validationMsg = 'The database service has more than one container running. Cannot import.';
}
if ( if (
$this->resource->getMorphClass() == 'App\Models\StandaloneRedis' $this->resource->getMorphClass() == 'App\Models\StandaloneRedis' ||
|| $this->resource->getMorphClass() == 'App\Models\StandaloneMongodb' $this->resource->getMorphClass() == 'App\Models\StandaloneKeydb' ||
$this->resource->getMorphClass() == 'App\Models\StandaloneDragonfly' ||
$this->resource->getMorphClass() == 'App\Models\StandaloneClickhouse' ||
$this->resource->getMorphClass() == 'App\Models\StandaloneMongodb'
) { ) {
$this->validated = false; $this->unsupported = true;
$this->validationMsg = 'This database type is not currently supported.';
} }
} }
public function runImport() public function runImport()
{ {
$this->validate([
'file' => 'required|file|max:102400'
]);
$this->importRunning = true;
$this->scpInProgress = true;
if ($this->filename == '') {
$this->dispatch('error', 'Please select a file to import.');
return;
}
try { try {
$uploadedFilename = $this->file->store('backup-import'); $uploadedFilename = "upload/{$this->resource->uuid}/restore";
$path = Storage::path($uploadedFilename); $path = Storage::path($uploadedFilename);
if (!Storage::exists($uploadedFilename)) {
$this->dispatch('error', 'The file does not exist or has been deleted.');
return;
}
$tmpPath = '/tmp/' . basename($uploadedFilename); $tmpPath = '/tmp/' . basename($uploadedFilename);
// SCP the backup file to the server.
instant_scp($path, $tmpPath, $this->server); instant_scp($path, $tmpPath, $this->server);
$this->scpInProgress = false; Storage::delete($uploadedFilename);
$this->importCommands[] = "docker cp {$tmpPath} {$this->container}:{$tmpPath}"; $this->importCommands[] = "docker cp {$tmpPath} {$this->container}:{$tmpPath}";
switch ($this->resource->getMorphClass()) { switch ($this->resource->getMorphClass()) {
@ -132,8 +111,7 @@ public function runImport()
$this->dispatch('activityMonitor', $activity->id); $this->dispatch('activityMonitor', $activity->id);
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->validated = false; return handleError($e, $this);
$this->validationMsg = $e->getMessage();
} }
} }
} }

View File

@ -0,0 +1,117 @@
<?php
namespace App\Livewire\Project\Database\Keydb;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Models\StandaloneKeydb;
use Exception;
use Livewire\Component;
class General extends Component
{
protected $listeners = ['refresh'];
public StandaloneKeydb $database;
public ?string $db_url = null;
public ?string $db_url_public = null;
protected $rules = [
'database.name' => 'required',
'database.description' => 'nullable',
'database.keydb_conf' => 'nullable',
'database.keydb_password' => 'required',
'database.image' => 'required',
'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
];
protected $validationAttributes = [
'database.name' => 'Name',
'database.description' => 'Description',
'database.keydb_conf' => 'Redis Configuration',
'database.keydb_password' => 'Redis Password',
'database.image' => 'Image',
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
];
public function mount()
{
$this->db_url = $this->database->get_db_url(true);
if ($this->database->is_public) {
$this->db_url_public = $this->database->get_db_url();
}
}
public function instantSaveAdvanced() {
try {
if (!$this->database->destination->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
return;
}
$this->database->save();
$this->dispatch('success', 'Database updated.');
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function submit()
{
try {
$this->validate();
if ($this->database->keydb_conf === "") {
$this->database->keydb_conf = null;
}
$this->database->save();
$this->dispatch('success', 'Database updated.');
} catch (Exception $e) {
return handleError($e, $this);
} finally {
if (is_null($this->database->config_hash)) {
$this->database->isConfigurationChanged(true);
} else {
$this->dispatch('configurationChanged');
}
}
}
public function instantSave()
{
try {
if ($this->database->is_public && !$this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
if (!str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
return;
}
StartDatabaseProxy::run($this->database);
$this->db_url_public = $this->database->get_db_url();
$this->dispatch('success', 'Database is now publicly accessible.');
} else {
StopDatabaseProxy::run($this->database);
$this->db_url_public = null;
$this->dispatch('success', 'Database is no longer publicly accessible.');
}
$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 render()
{
return view('livewire.project.database.keydb.general');
}
}

View File

@ -76,6 +76,12 @@ public function submit()
$this->dispatch('success', 'Database updated.'); $this->dispatch('success', 'Database updated.');
} catch (Exception $e) { } catch (Exception $e) {
return handleError($e, $this); return handleError($e, $this);
} finally {
if (is_null($this->database->config_hash)) {
$this->database->isConfigurationChanged(true);
} else {
$this->dispatch('configurationChanged');
}
} }
} }
public function instantSave() public function instantSave()

View File

@ -78,6 +78,12 @@ public function submit()
$this->dispatch('success', 'Database updated.'); $this->dispatch('success', 'Database updated.');
} catch (Exception $e) { } catch (Exception $e) {
return handleError($e, $this); return handleError($e, $this);
} finally {
if (is_null($this->database->config_hash)) {
$this->database->isConfigurationChanged(true);
} else {
$this->dispatch('configurationChanged');
}
} }
} }
public function instantSave() public function instantSave()

View File

@ -77,6 +77,12 @@ public function submit()
$this->dispatch('success', 'Database updated.'); $this->dispatch('success', 'Database updated.');
} catch (Exception $e) { } catch (Exception $e) {
return handleError($e, $this); return handleError($e, $this);
} finally {
if (is_null($this->database->config_hash)) {
$this->database->isConfigurationChanged(true);
} else {
$this->dispatch('configurationChanged');
}
} }
} }
public function instantSave() public function instantSave()

View File

@ -58,7 +58,8 @@ public function mount()
$this->db_url_public = $this->database->get_db_url(); $this->db_url_public = $this->database->get_db_url();
} }
} }
public function instantSaveAdvanced() { public function instantSaveAdvanced()
{
try { try {
if (!$this->database->destination->server->isLogDrainEnabled()) { if (!$this->database->destination->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false; $this->database->is_log_drain_enabled = false;
@ -164,6 +165,12 @@ public function submit()
$this->dispatch('success', 'Database updated.'); $this->dispatch('success', 'Database updated.');
} catch (Exception $e) { } catch (Exception $e) {
return handleError($e, $this); return handleError($e, $this);
} finally {
if (is_null($this->database->config_hash)) {
$this->database->isConfigurationChanged(true);
} else {
$this->dispatch('configurationChanged');
}
} }
} }
} }

View File

@ -120,6 +120,9 @@ public function setType(string $type)
case 'mysql': case 'mysql':
case 'mariadb': case 'mariadb':
case 'redis': case 'redis':
case 'keydb':
case 'dragonfly':
case 'clickhouse':
case 'mongodb': case 'mongodb':
$this->isDatabase = true; $this->isDatabase = true;
$this->includeSwarm = false; $this->includeSwarm = false;

View File

@ -36,6 +36,12 @@ public function mount()
$database = create_standalone_mysql($environment->id, $destination_uuid); $database = create_standalone_mysql($environment->id, $destination_uuid);
} else if ($type->value() === 'mariadb') { } else if ($type->value() === 'mariadb') {
$database = create_standalone_mariadb($environment->id, $destination_uuid); $database = create_standalone_mariadb($environment->id, $destination_uuid);
} else if ($type->value() === 'keydb') {
$database = create_standalone_keydb($environment->id, $destination_uuid);
} else if ($type->value() === 'dragonfly') {
$database = create_standalone_dragonfly($environment->id, $destination_uuid);
}else if ($type->value() === 'clickhouse') {
$database = create_standalone_clickhouse($environment->id, $destination_uuid);
} }
return redirect()->route('project.database.configuration', [ return redirect()->route('project.database.configuration', [
'project_uuid' => $project->uuid, 'project_uuid' => $project->uuid,

View File

@ -16,6 +16,9 @@ class Index extends Component
public $mongodbs = []; public $mongodbs = [];
public $mysqls = []; public $mysqls = [];
public $mariadbs = []; public $mariadbs = [];
public $keydbs = [];
public $dragonflies = [];
public $clickhouses = [];
public $services = []; public $services = [];
public function mount() public function mount()
{ {
@ -96,6 +99,39 @@ public function mount()
} }
return $mariadb; return $mariadb;
}); });
$this->keydbs = $this->environment->keydbs->load(['tags'])->sortBy('name');
$this->keydbs = $this->keydbs->map(function ($keydb) {
if (data_get($keydb, 'environment.project.uuid')) {
$keydb->hrefLink = route('project.database.configuration', [
'project_uuid' => data_get($keydb, 'environment.project.uuid'),
'environment_name' => data_get($keydb, 'environment.name'),
'database_uuid' => data_get($keydb, 'uuid')
]);
}
return $keydb;
});
$this->dragonflies = $this->environment->dragonflies->load(['tags'])->sortBy('name');
$this->dragonflies = $this->dragonflies->map(function ($dragonfly) {
if (data_get($dragonfly, 'environment.project.uuid')) {
$dragonfly->hrefLink = route('project.database.configuration', [
'project_uuid' => data_get($dragonfly, 'environment.project.uuid'),
'environment_name' => data_get($dragonfly, 'environment.name'),
'database_uuid' => data_get($dragonfly, 'uuid')
]);
}
return $dragonfly;
});
$this->clickhouses = $this->environment->clickhouses->load(['tags'])->sortBy('name');
$this->clickhouses = $this->clickhouses->map(function ($clickhouse) {
if (data_get($clickhouse, 'environment.project.uuid')) {
$clickhouse->hrefLink = route('project.database.configuration', [
'project_uuid' => data_get($clickhouse, 'environment.project.uuid'),
'environment_name' => data_get($clickhouse, 'environment.name'),
'database_uuid' => data_get($clickhouse, 'uuid')
]);
}
return $clickhouse;
});
$this->services = $this->environment->services->load(['tags'])->sortBy('name'); $this->services = $this->environment->services->load(['tags'])->sortBy('name');
$this->services = $this->services->map(function ($service) { $this->services = $this->services->map(function ($service) {
if (data_get($service, 'environment.project.uuid')) { if (data_get($service, 'environment.project.uuid')) {

View File

@ -18,7 +18,8 @@ public function getListeners()
$userId = auth()->user()->id; $userId = auth()->user()->id;
return [ return [
"echo-private:user.{$userId},ServiceStatusChanged" => 'check_status', "echo-private:user.{$userId},ServiceStatusChanged" => 'check_status',
"check_status" "check_status",
"refresh" => '$refresh',
]; ];
} }
public function render() public function render()
@ -65,7 +66,7 @@ public function check_status()
try { try {
dispatch_sync(new ContainerStatusJob($this->service->server)); dispatch_sync(new ContainerStatusJob($this->service->server));
$this->dispatch('refresh')->self(); $this->dispatch('refresh')->self();
$this->dispatch('serviceStatusChanged'); $this->dispatch('updateStatus');
} catch (\Exception $e) { } catch (\Exception $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@ -18,7 +18,7 @@ public function mount() {
} }
public function saveEditedCompose() { public function saveEditedCompose() {
$this->dispatch('warning', "Saving new docker compose..."); $this->dispatch('info', "Saving new docker compose...");
$this->dispatch('saveCompose', $this->service->docker_compose_raw); $this->dispatch('saveCompose', $this->service->docker_compose_raw);
} }
public function render() public function render()

View File

@ -0,0 +1,53 @@
<?php
namespace App\Livewire\Project\Service;
use App\Models\ServiceApplication;
use Livewire\Component;
class EditDomain extends Component
{
public $applicationId;
public ServiceApplication $application;
protected $rules = [
'application.fqdn' => 'nullable',
'application.required_fqdn' => 'required|boolean',
];
public function mount() {
$this->application = ServiceApplication::find($this->applicationId);
}
public function updatedApplicationFqdn()
{
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
return str($domain)->trim()->lower();
});
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
$this->application->save();
}
public function submit()
{
try {
check_domain_usage(resource: $this->application);
$this->validate();
$this->application->save();
updateCompose($this->application);
if (str($this->application->fqdn)->contains(',')) {
$this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.<br><br>Only use multiple domains if you know what you are doing.');
} else {
$this->dispatch('success', 'Service saved.');
}
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->dispatch('generateDockerCompose');
$this->dispatch('refresh');
$this->dispatch('configurationChanged');
}
}
public function render()
{
return view('livewire.project.service.edit-domain');
}
}

View File

@ -5,13 +5,14 @@
use App\Models\LocalFileVolume; use App\Models\LocalFileVolume;
use App\Models\ServiceApplication; use App\Models\ServiceApplication;
use App\Models\ServiceDatabase; use App\Models\ServiceDatabase;
use App\Models\StandaloneClickhouse;
use Livewire\Component; use Livewire\Component;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class FileStorage extends Component class FileStorage extends Component
{ {
public LocalFileVolume $fileStorage; public LocalFileVolume $fileStorage;
public ServiceApplication|ServiceDatabase $service; public ServiceApplication|ServiceDatabase|StandaloneClickhouse $resource;
public string $fs_path; public string $fs_path;
public ?string $workdir = null; public ?string $workdir = null;
@ -23,14 +24,14 @@ class FileStorage extends Component
]; ];
public function mount() public function mount()
{ {
$this->service = $this->fileStorage->service; $this->resource = $this->fileStorage->service;
if (Str::of($this->fileStorage->fs_path)->startsWith('.')) { if (Str::of($this->fileStorage->fs_path)->startsWith('.')) {
$this->workdir = $this->service->service->workdir(); $this->workdir = $this->resource->service->workdir();
$this->fs_path = Str::of($this->fileStorage->fs_path)->after('.'); $this->fs_path = Str::of($this->fileStorage->fs_path)->after('.');
} else { } else {
$this->workdir = null; $this->workdir = null;
$this->fs_path = $this->fileStorage->fs_path; $this->fs_path = $this->fileStorage->fs_path;
} }
} }
public function submit() public function submit()
{ {

View File

@ -17,21 +17,33 @@ class Navbar extends Component
public array $query; public array $query;
public $isDeploymentProgress = false; public $isDeploymentProgress = false;
public function mount()
{
if (str($this->service->status())->contains('running') && is_null($this->service->config_hash)) {
ray('isConfigurationChanged init');
$this->service->isConfigurationChanged(true);
$this->dispatch('configurationChanged');
}
}
public function getListeners() public function getListeners()
{ {
$userId = auth()->user()->id; $userId = auth()->user()->id;
return [ return [
"echo-private:user.{$userId},ServiceStatusChanged" => 'serviceStarted', "echo-private:user.{$userId},ServiceStatusChanged" => 'serviceStarted',
"serviceStatusChanged" "updateStatus"=> '$refresh',
]; ];
} }
public function serviceStarted() { public function serviceStarted()
$this->dispatch('success', 'Service status changed.');
}
public function serviceStatusChanged()
{ {
$this->dispatch('refresh')->self(); $this->dispatch('success', 'Service status changed.');
if (is_null($this->service->config_hash) || $this->service->isConfigurationChanged()) {
$this->service->isConfigurationChanged(true);
$this->dispatch('configurationChanged');
} else {
$this->dispatch('configurationChanged');
}
} }
public function check_status() public function check_status()
{ {
$this->dispatch('check_status'); $this->dispatch('check_status');

View File

@ -69,6 +69,13 @@ public function submit()
$this->dispatch('success', 'Service saved.'); $this->dispatch('success', 'Service saved.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} finally {
if (is_null($this->service->config_hash)) {
ray('asdf');
$this->service->isConfigurationChanged(true);
} else {
$this->dispatch('configurationChanged');
}
} }
} }
public function render() public function render()

View File

@ -0,0 +1,34 @@
<?php
namespace App\Livewire\Project\Shared;
use App\Models\Application;
use App\Models\Service;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Livewire\Component;
class ConfigurationChecker extends Component
{
public bool $isConfigurationChanged = false;
public Application|Service|StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource;
protected $listeners = ['configurationChanged'];
public function mount()
{
$this->configurationChanged();
}
public function render()
{
return view('livewire.project.shared.configuration-checker');
}
public function configurationChanged()
{
$this->isConfigurationChanged = $this->resource->isConfigurationChanged();
}
}

View File

@ -11,6 +11,7 @@ class Danger extends Component
public $resource; public $resource;
public $projectUuid; public $projectUuid;
public $environmentName; public $environmentName;
public bool $delete_configurations = true;
public ?string $modalId = null; public ?string $modalId = null;
public function mount() public function mount()
@ -20,12 +21,11 @@ public function mount()
$this->projectUuid = data_get($parameters, 'project_uuid'); $this->projectUuid = data_get($parameters, 'project_uuid');
$this->environmentName = data_get($parameters, 'environment_name'); $this->environmentName = data_get($parameters, 'environment_name');
} }
public function delete() public function delete()
{ {
try { try {
$this->resource->delete(); $this->resource->delete();
DeleteResourceJob::dispatch($this->resource); DeleteResourceJob::dispatch($this->resource, $this->delete_configurations);
return redirect()->route('project.resource.index', [ return redirect()->route('project.resource.index', [
'project_uuid' => $this->projectUuid, 'project_uuid' => $this->projectUuid,
'environment_name' => $this->environmentName 'environment_name' => $this->environmentName

View File

@ -119,6 +119,15 @@ public function saveVariables($isPreview)
case 'standalone-mariadb': case 'standalone-mariadb':
$environment->standalone_mariadb_id = $this->resource->id; $environment->standalone_mariadb_id = $this->resource->id;
break; break;
case 'standalone-keydb':
$environment->standalone_keydb_id = $this->resource->id;
break;
case 'standalone-dragonfly':
$environment->standalone_dragonfly_id = $this->resource->id;
break;
case 'standalone-clickhouse':
$environment->standalone_clickhouse_id = $this->resource->id;
break;
case 'service': case 'service':
$environment->service_id = $this->resource->id; $environment->service_id = $this->resource->id;
break; break;
@ -173,6 +182,15 @@ public function submit($data)
case 'standalone-mariadb': case 'standalone-mariadb':
$environment->standalone_mariadb_id = $this->resource->id; $environment->standalone_mariadb_id = $this->resource->id;
break; break;
case 'standalone-keydb':
$environment->standalone_keydb_id = $this->resource->id;
break;
case 'standalone-dragonfly':
$environment->standalone_dragonfly_id = $this->resource->id;
break;
case 'standalone-clickhouse':
$environment->standalone_clickhouse_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

@ -5,11 +5,6 @@
use App\Models\Application; use App\Models\Application;
use App\Models\Server; use App\Models\Server;
use App\Models\Service; use App\Models\Service;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Livewire\Component; use Livewire\Component;
@ -50,21 +45,9 @@ public function mount()
} }
} else if (data_get($this->parameters, 'database_uuid')) { } else if (data_get($this->parameters, 'database_uuid')) {
$this->type = 'database'; $this->type = 'database';
$resource = StandalonePostgresql::where('uuid', $this->parameters['database_uuid'])->first(); $resource = getResourceByUuid($this->parameters['database_uuid'], data_get(auth()->user()->currentTeam(),'id'));
if (is_null($resource)) { if (is_null($resource)) {
$resource = StandaloneRedis::where('uuid', $this->parameters['database_uuid'])->first(); abort(404);
if (is_null($resource)) {
$resource = StandaloneMongodb::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
$resource = StandaloneMysql::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
$resource = StandaloneMariadb::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
abort(404);
}
}
}
}
} }
$this->resource = $resource; $this->resource = $resource;
if ($this->resource->destination->server->isFunctional()) { if ($this->resource->destination->server->isFunctional()) {
@ -109,7 +92,7 @@ public function loadContainers()
]; ];
$this->containers = $this->containers->push($payload); $this->containers = $this->containers->push($payload);
} }
} }
} }
if ($this->containers->count() > 0) { if ($this->containers->count() > 0) {
if (data_get($this->parameters, 'application_uuid')) { if (data_get($this->parameters, 'application_uuid')) {

View File

@ -7,6 +7,9 @@
use App\Models\Service; use App\Models\Service;
use App\Models\ServiceApplication; use App\Models\ServiceApplication;
use App\Models\ServiceDatabase; use App\Models\ServiceDatabase;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
use App\Models\StandaloneMariadb; use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb; use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql; use App\Models\StandaloneMysql;
@ -19,7 +22,7 @@ class GetLogs extends Component
{ {
public string $outputs = ''; public string $outputs = '';
public string $errors = ''; public string $errors = '';
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|null $resource = null; public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse|null $resource = null;
public ServiceApplication|ServiceDatabase|null $servicesubtype = null; public ServiceApplication|ServiceDatabase|null $servicesubtype = null;
public Server $server; public Server $server;
public ?string $container = null; public ?string $container = null;

View File

@ -5,6 +5,9 @@
use App\Models\Application; use App\Models\Application;
use App\Models\Server; use App\Models\Server;
use App\Models\Service; use App\Models\Service;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
use App\Models\StandaloneMariadb; use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb; use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql; use App\Models\StandaloneMysql;
@ -16,7 +19,7 @@
class Logs extends Component class Logs extends Component
{ {
public ?string $type = null; public ?string $type = null;
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $resource; public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource;
public Collection $servers; public Collection $servers;
public Collection $containers; public Collection $containers;
public $container = []; public $container = [];
@ -67,21 +70,9 @@ public function mount()
} }
} else if (data_get($this->parameters, 'database_uuid')) { } else if (data_get($this->parameters, 'database_uuid')) {
$this->type = 'database'; $this->type = 'database';
$resource = StandalonePostgresql::where('uuid', $this->parameters['database_uuid'])->first(); $resource = getResourceByUuid($this->parameters['database_uuid'], data_get(auth()->user()->currentTeam(), 'id'));
if (is_null($resource)) { if (is_null($resource)) {
$resource = StandaloneRedis::where('uuid', $this->parameters['database_uuid'])->first(); abort(404);
if (is_null($resource)) {
$resource = StandaloneMongodb::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
$resource = StandaloneMysql::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
$resource = StandaloneMariadb::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
abort(404);
}
}
}
}
} }
$this->resource = $resource; $this->resource = $resource;
$this->status = $this->resource->status; $this->status = $this->resource->status;

View File

@ -76,7 +76,10 @@ public function cloneTo($destination_id)
$this->resource->getMorphClass() === 'App\Models\StandaloneMongodb' || $this->resource->getMorphClass() === 'App\Models\StandaloneMongodb' ||
$this->resource->getMorphClass() === 'App\Models\StandaloneMysql' || $this->resource->getMorphClass() === 'App\Models\StandaloneMysql' ||
$this->resource->getMorphClass() === 'App\Models\StandaloneMariadb' || $this->resource->getMorphClass() === 'App\Models\StandaloneMariadb' ||
$this->resource->getMorphClass() === 'App\Models\StandaloneRedis' $this->resource->getMorphClass() === 'App\Models\StandaloneRedis' ||
$this->resource->getMorphClass() === 'App\Models\StandaloneKeydb' ||
$this->resource->getMorphClass() === 'App\Models\StandaloneDragonfly' ||
$this->resource->getMorphClass() === 'App\Models\StandaloneClickhouse'
) { ) {
$uuid = (string)new Cuid2(7); $uuid = (string)new Cuid2(7);
$new_resource = $this->resource->replicate()->fill([ $new_resource = $this->resource->replicate()->fill([

View File

@ -2,7 +2,6 @@
namespace App\Livewire\Project\Shared\Storages; namespace App\Livewire\Project\Shared\Storages;
use App\Models\LocalPersistentVolume;
use Livewire\Component; use Livewire\Component;
class All extends Component class All extends Component

View File

@ -75,6 +75,7 @@ public function revalidate()
} }
public function checkLocalhostConnection() public function checkLocalhostConnection()
{ {
$this->submit();
$uptime = $this->server->validateConnection(); $uptime = $this->server->validateConnection();
if ($uptime) { if ($uptime) {
$this->dispatch('success', 'Server is reachable.'); $this->dispatch('success', 'Server is reachable.');

View File

@ -57,6 +57,15 @@ protected static function booted()
}); });
} }
public function delete_configurations()
{
$server = data_get($this, 'destination.server');
$workdir = $this->workdir();
if (str($workdir)->endsWith($this->uuid)) {
ray('Deleting workdir');
instant_remote_process(["rm -rf " . $this->workdir()], $server, false);
}
}
public function additional_servers() public function additional_servers()
{ {
return $this->belongsToMany(Server::class, 'additional_destinations') return $this->belongsToMany(Server::class, 'additional_destinations')
@ -500,7 +509,7 @@ public function isLogDrainEnabled()
} }
public function isConfigurationChanged(bool $save = false) public function isConfigurationChanged(bool $save = false)
{ {
$newConfigHash = $this->fqdn . $this->git_repository . $this->git_branch . $this->git_commit_sha . $this->build_pack . $this->static_image . $this->install_command . $this->build_command . $this->start_command . $this->port_exposes . $this->port_mappings . $this->base_directory . $this->publish_directory . $this->dockerfile . $this->dockerfile_location . $this->custom_labels; $newConfigHash = $this->fqdn . $this->git_repository . $this->git_branch . $this->git_commit_sha . $this->build_pack . $this->static_image . $this->install_command . $this->build_command . $this->start_command . $this->ports_exposes . $this->ports_mappings . $this->base_directory . $this->publish_directory . $this->dockerfile . $this->dockerfile_location . $this->custom_labels . $this->custom_docker_run_options . $this->dockerfile_target_build;
if ($this->pull_request_id === 0 || $this->pull_request_id === null) { if ($this->pull_request_id === 0 || $this->pull_request_id === null) {
$newConfigHash .= json_encode($this->environment_variables()->get('updated_at')); $newConfigHash .= json_encode($this->environment_variables()->get('updated_at'));
} else { } else {

View File

@ -46,7 +46,18 @@ public function mariadbs()
{ {
return $this->hasMany(StandaloneMariadb::class); return $this->hasMany(StandaloneMariadb::class);
} }
public function keydbs()
{
return $this->hasMany(StandaloneKeydb::class);
}
public function dragonflies()
{
return $this->hasMany(StandaloneDragonfly::class);
}
public function clickhouses()
{
return $this->hasMany(StandaloneClickhouse::class);
}
public function databases() public function databases()
{ {
$postgresqls = $this->postgresqls; $postgresqls = $this->postgresqls;
@ -54,7 +65,10 @@ public function databases()
$mongodbs = $this->mongodbs; $mongodbs = $this->mongodbs;
$mysqls = $this->mysqls; $mysqls = $this->mysqls;
$mariadbs = $this->mariadbs; $mariadbs = $this->mariadbs;
return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs); $keydbs = $this->keydbs;
$dragonflies = $this->dragonflies;
$clickhouses = $this->clickhouses;
return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs)->concat($keydbs)->concat($dragonflies)->concat($clickhouses);
} }
public function project() public function project()

View File

@ -32,7 +32,7 @@ protected static function booted()
'key' => $environment_variable->key, 'key' => $environment_variable->key,
'value' => $environment_variable->value, 'value' => $environment_variable->value,
'is_build_time' => $environment_variable->is_build_time, 'is_build_time' => $environment_variable->is_build_time,
'is_multiline' => $environment_variable->is_multiline, 'is_multiline' => $environment_variable->is_multiline ?? false,
'application_id' => $environment_variable->application_id, 'application_id' => $environment_variable->application_id,
'is_preview' => true 'is_preview' => true
]); ]);
@ -63,19 +63,7 @@ public function resource()
} else if ($this->service_id) { } else if ($this->service_id) {
$resource = Service::find($this->service_id); $resource = Service::find($this->service_id);
} else if ($this->database_id) { } else if ($this->database_id) {
$resource = StandalonePostgresql::find($this->database_id); $resource = getResourceByUuid($this->parameters['database_uuid'], data_get(auth()->user()->currentTeam(), 'id'));
if (!$resource) {
$resource = StandaloneMysql::find($this->database_id);
if (!$resource) {
$resource = StandaloneRedis::find($this->database_id);
if (!$resource) {
$resource = StandaloneMongodb::find($this->database_id);
if (!$resource) {
$resource = StandaloneMariadb::find($this->database_id);
}
}
}
}
} }
return $resource; return $resource;
} }

View File

@ -22,8 +22,14 @@ public function service()
} }
public function saveStorageOnServer() public function saveStorageOnServer()
{ {
$workdir = $this->resource->service->workdir(); $isService = data_get($this->resource, 'service');
$server = $this->resource->service->server; if ($isService) {
$workdir = $this->resource->service->workdir();
$server = $this->resource->service->server;
} else {
$workdir = $this->resource->workdir();
$server = $this->resource->destination->server;
}
$commands = collect([ $commands = collect([
"mkdir -p $workdir > /dev/null 2>&1 || true", "mkdir -p $workdir > /dev/null 2>&1 || true",
"cd $workdir" "cd $workdir"
@ -55,8 +61,18 @@ public function saveStorageOnServer()
if (!$fileVolume->is_directory && $isDir == 'NOK') { if (!$fileVolume->is_directory && $isDir == 'NOK') {
if ($content) { if ($content) {
$content = base64_encode($content); $content = base64_encode($content);
$chmod = $fileVolume->chmod;
$chown = $fileVolume->chown;
ray($content, $path, $chmod, $chown);
$commands->push("echo '$content' | base64 -d > $path"); $commands->push("echo '$content' | base64 -d > $path");
$commands->push("chmod +x $path"); $commands->push("chmod +x $path");
if ($chown) {
$commands->push("chown $chown $path");
}
if ($chmod) {
$commands->push("chmod $chmod $path");
}
} }
} else if ($isDir == 'NOK' && $fileVolume->is_directory) { } else if ($isDir == 'NOK' && $fileVolume->is_directory) {
$commands->push("mkdir -p $path > /dev/null 2>&1 || true"); $commands->push("mkdir -p $path > /dev/null 2>&1 || true");

View File

@ -63,6 +63,18 @@ public function redis()
{ {
return $this->hasManyThrough(StandaloneRedis::class, Environment::class); return $this->hasManyThrough(StandaloneRedis::class, Environment::class);
} }
public function keydbs()
{
return $this->hasManyThrough(StandaloneKeydb::class, Environment::class);
}
public function dragonflies()
{
return $this->hasManyThrough(StandaloneDragonfly::class, Environment::class);
}
public function clickhouses()
{
return $this->hasManyThrough(StandaloneClickhouse::class, Environment::class);
}
public function mongodbs() public function mongodbs()
{ {
return $this->hasManyThrough(StandaloneMongodb::class, Environment::class); return $this->hasManyThrough(StandaloneMongodb::class, Environment::class);
@ -77,6 +89,6 @@ public function mariadbs()
} }
public function resource_count() public function resource_count()
{ {
return $this->applications()->count() + $this->postgresqls()->count() + $this->redis()->count() + $this->mongodbs()->count() + $this->mysqls()->count() + $this->mariadbs()->count(); return $this->applications()->count() + $this->postgresqls()->count() + $this->redis()->count() + $this->mongodbs()->count() + $this->mysqls()->count() + $this->mariadbs()->count() + $this->keydbs()->count() + $this->dragonflies()->count() + $this->services()->count() + $this->clickhouses()->count();
} }
} }

View File

@ -585,7 +585,10 @@ public function databases()
$mongodbs = data_get($standaloneDocker, 'mongodbs', collect([])); $mongodbs = data_get($standaloneDocker, 'mongodbs', collect([]));
$mysqls = data_get($standaloneDocker, 'mysqls', collect([])); $mysqls = data_get($standaloneDocker, 'mysqls', collect([]));
$mariadbs = data_get($standaloneDocker, 'mariadbs', collect([])); $mariadbs = data_get($standaloneDocker, 'mariadbs', collect([]));
return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs); $keydbs = data_get($standaloneDocker, 'keydbs', collect([]));
$dragonflies = data_get($standaloneDocker, 'dragonflies', collect([]));
$clickhouses = data_get($standaloneDocker, 'clickhouses', collect([]));
return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs)->concat($keydbs)->concat($dragonflies)->concat($clickhouses);
})->filter(function ($item) { })->filter(function ($item) {
return data_get($item, 'name') !== 'coolify-db'; return data_get($item, 'name') !== 'coolify-db';
})->flatten(); })->flatten();

View File

@ -11,6 +11,46 @@ class Service extends BaseModel
{ {
use HasFactory, SoftDeletes; use HasFactory, SoftDeletes;
protected $guarded = []; protected $guarded = [];
public function isConfigurationChanged(bool $save = false)
{
$domains = $this->applications()->get()->pluck('fqdn')->toArray();
$domains = implode(',', $domains);
$applicationImages = $this->applications()->get()->pluck('image');
$databaseImages = $this->databases()->get()->pluck('image');
$images = $applicationImages->merge($databaseImages);
$images = implode(',', $images->toArray());
$applicationStorages = $this->applications()->get()->pluck('persistentStorages')->flatten();
$databaseStorages = $this->databases()->get()->pluck('persistentStorages')->flatten();
$storages = $applicationStorages->merge($databaseStorages)->implode('updated_at');
$newConfigHash = $images . $domains . $images . $storages;
$newConfigHash .= json_encode($this->environment_variables()->get('value'));
$newConfigHash = md5($newConfigHash);
$oldConfigHash = data_get($this, 'config_hash');
if ($oldConfigHash === null) {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
if ($oldConfigHash === $newConfigHash) {
return false;
} else {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
}
public function isExited()
{
return (bool) str($this->status())->contains('exited');
}
public function type() public function type()
{ {
return 'service'; return 'service';
@ -27,6 +67,14 @@ public function tags()
{ {
return $this->morphToMany(Tag::class, 'taggable'); return $this->morphToMany(Tag::class, 'taggable');
} }
public function delete_configurations()
{
$server = data_get($this, 'server');
$workdir = $this->workdir();
if (str($workdir)->endsWith($this->uuid)) {
instant_remote_process(["rm -rf " . $this->workdir()], $server, false);
}
}
public function status() public function status()
{ {
$applications = $this->applications; $applications = $this->applications;

View File

@ -0,0 +1,210 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneClickhouse extends BaseModel
{
use HasFactory, SoftDeletes;
protected $guarded = [];
protected $casts = [
'clickhouse_password' => 'encrypted',
];
protected static function booted()
{
static::created(function ($database) {
LocalPersistentVolume::create([
'name' => 'clickhouse-data-' . $database->uuid,
'mount_path' => '/bitnami/clickhouse',
'host_path' => null,
'resource_id' => $database->id,
'resource_type' => $database->getMorphClass(),
'is_readonly' => true
]);
});
static::deleting(function ($database) {
$storages = $database->persistentStorages()->get();
$server = data_get($database, 'destination.server');
if ($server) {
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
$database->scheduledBackups()->delete();
$database->persistentStorages()->delete();
$database->environment_variables()->delete();
$database->tags()->detach();
});
}
public function isConfigurationChanged(bool $save = false)
{
$newConfigHash = $this->image . $this->ports_mappings;
$newConfigHash .= json_encode($this->environment_variables()->get('updated_at'));
$newConfigHash = md5($newConfigHash);
$oldConfigHash = data_get($this, 'config_hash');
if ($oldConfigHash === null) {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
if ($oldConfigHash === $newConfigHash) {
return false;
} else {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
}
public function isExited()
{
return (bool) str($this->status)->startsWith('exited');
}
public function workdir()
{
return database_configuration_dir() . "/{$this->uuid}";
}
public function delete_configurations()
{
$server = data_get($this, 'destination.server');
$workdir = $this->workdir();
if (str($workdir)->endsWith($this->uuid)) {
instant_remote_process(["rm -rf " . $this->workdir()], $server, false);
}
}
public function realStatus()
{
return $this->getRawOriginal('status');
}
public function status(): Attribute
{
return Attribute::make(
set: function ($value) {
if (str($value)->contains('(')) {
$status = str($value)->before('(')->trim()->value();
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
} else if (str($value)->contains(':')) {
$status = str($value)->before(':')->trim()->value();
$health = str($value)->after(':')->trim()->value() ?? 'unhealthy';
} else {
$status = $value;
$health = 'unhealthy';
}
return "$status:$health";
},
get: function ($value) {
if (str($value)->contains('(')) {
$status = str($value)->before('(')->trim()->value();
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
} else if (str($value)->contains(':')) {
$status = str($value)->before(':')->trim()->value();
$health = str($value)->after(':')->trim()->value() ?? 'unhealthy';
} else {
$status = $value;
$health = 'unhealthy';
}
return "$status:$health";
},
);
}
public function tags()
{
return $this->morphToMany(Tag::class, 'taggable');
}
public function project()
{
return data_get($this, 'environment.project');
}
public function link()
{
if (data_get($this, 'environment.project.uuid')) {
return route('project.database.configuration', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'database_uuid' => data_get($this, 'uuid')
]);
}
return null;
}
public function isLogDrainEnabled()
{
return data_get($this, 'is_log_drain_enabled', false);
}
public function portsMappings(): Attribute
{
return Attribute::make(
set: fn ($value) => $value === "" ? null : $value,
);
}
public function portsMappingsArray(): Attribute
{
return Attribute::make(
get: fn () => is_null($this->ports_mappings)
? []
: explode(',', $this->ports_mappings),
);
}
public function team()
{
return data_get($this, 'environment.project.team');
}
public function type(): string
{
return 'standalone-clickhouse';
}
public function get_db_url(bool $useInternal = false): string
{
if ($this->is_public && !$useInternal) {
return "clickhouse://{$this->clickhouse_user}:{$this->clickhouse_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->clickhouse_db}";
} else {
return "clickhouse://{$this->clickhouse_user}:{$this->clickhouse_password}@{$this->uuid}:9000/{$this->clickhouse_db}";
}
}
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

@ -32,6 +32,18 @@ public function mariadbs()
{ {
return $this->morphMany(StandaloneMariadb::class, 'destination'); return $this->morphMany(StandaloneMariadb::class, 'destination');
} }
public function keydbs()
{
return $this->morphMany(StandaloneKeydb::class, 'destination');
}
public function dragonflies()
{
return $this->morphMany(StandaloneDragonfly::class, 'destination');
}
public function clickhouses()
{
return $this->morphMany(StandaloneClickhouse::class, 'destination');
}
public function server() public function server()
{ {

View File

@ -0,0 +1,210 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneDragonfly extends BaseModel
{
use HasFactory, SoftDeletes;
protected $guarded = [];
protected $casts = [
'dragonfly_password' => 'encrypted',
];
protected static function booted()
{
static::created(function ($database) {
LocalPersistentVolume::create([
'name' => 'dragonfly-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();
$server = data_get($database, 'destination.server');
if ($server) {
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
$database->persistentStorages()->delete();
$database->environment_variables()->delete();
$database->tags()->detach();
});
}
public function isConfigurationChanged(bool $save = false)
{
$newConfigHash = $this->image . $this->ports_mappings;
$newConfigHash .= json_encode($this->environment_variables()->get('updated_at'));
$newConfigHash = md5($newConfigHash);
$oldConfigHash = data_get($this, 'config_hash');
if ($oldConfigHash === null) {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
if ($oldConfigHash === $newConfigHash) {
return false;
} else {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
}
public function isExited()
{
return (bool) str($this->status)->startsWith('exited');
}
public function workdir()
{
return database_configuration_dir() . "/{$this->uuid}";
}
public function delete_configurations()
{
$server = data_get($this, 'destination.server');
$workdir = $this->workdir();
if (str($workdir)->endsWith($this->uuid)) {
instant_remote_process(["rm -rf " . $this->workdir()], $server, false);
}
}
public function realStatus()
{
return $this->getRawOriginal('status');
}
public function status(): Attribute
{
return Attribute::make(
set: function ($value) {
if (str($value)->contains('(')) {
$status = str($value)->before('(')->trim()->value();
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
} else if (str($value)->contains(':')) {
$status = str($value)->before(':')->trim()->value();
$health = str($value)->after(':')->trim()->value() ?? 'unhealthy';
} else {
$status = $value;
$health = 'unhealthy';
}
return "$status:$health";
},
get: function ($value) {
if (str($value)->contains('(')) {
$status = str($value)->before('(')->trim()->value();
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
} else if (str($value)->contains(':')) {
$status = str($value)->before(':')->trim()->value();
$health = str($value)->after(':')->trim()->value() ?? 'unhealthy';
} else {
$status = $value;
$health = 'unhealthy';
}
return "$status:$health";
},
);
}
public function tags()
{
return $this->morphToMany(Tag::class, 'taggable');
}
public function project()
{
return data_get($this, 'environment.project');
}
public function team()
{
return data_get($this, 'environment.project.team');
}
public function link()
{
if (data_get($this, 'environment.project.uuid')) {
return route('project.database.configuration', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'database_uuid' => data_get($this, 'uuid')
]);
}
return null;
}
public function isLogDrainEnabled()
{
return data_get($this, 'is_log_drain_enabled', false);
}
public function portsMappings(): Attribute
{
return Attribute::make(
set: fn ($value) => $value === "" ? null : $value,
);
}
public function portsMappingsArray(): Attribute
{
return Attribute::make(
get: fn () => is_null($this->ports_mappings)
? []
: explode(',', $this->ports_mappings),
);
}
public function type(): string
{
return 'standalone-dragonfly';
}
public function get_db_url(bool $useInternal = false): string
{
if ($this->is_public && !$useInternal) {
return "redis://{$this->dragonfly_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
} else {
return "redis://{$this->dragonfly_password}@{$this->uuid}:6379/0";
}
}
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

@ -0,0 +1,212 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneKeydb extends BaseModel
{
use HasFactory, SoftDeletes;
protected $guarded = [];
protected $casts = [
'keydb_password' => 'encrypted',
];
protected static function booted()
{
static::created(function ($database) {
LocalPersistentVolume::create([
'name' => 'keydb-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();
$server = data_get($database, 'destination.server');
if ($server) {
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
$database->persistentStorages()->delete();
$database->environment_variables()->delete();
$database->tags()->detach();
});
}
public function isConfigurationChanged(bool $save = false)
{
$newConfigHash = $this->image . $this->ports_mappings . $this->keydb_conf;
$newConfigHash .= json_encode($this->environment_variables()->get('updated_at'));
$newConfigHash = md5($newConfigHash);
$oldConfigHash = data_get($this, 'config_hash');
if ($oldConfigHash === null) {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
if ($oldConfigHash === $newConfigHash) {
return false;
} else {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
}
public function isExited()
{
return (bool) str($this->status)->startsWith('exited');
}
public function workdir()
{
return database_configuration_dir() . "/{$this->uuid}";
}
public function delete_configurations()
{
$server = data_get($this, 'destination.server');
$workdir = $this->workdir();
if (str($workdir)->endsWith($this->uuid)) {
instant_remote_process(["rm -rf " . $this->workdir()], $server, false);
}
}
public function realStatus()
{
return $this->getRawOriginal('status');
}
public function status(): Attribute
{
return Attribute::make(
set: function ($value) {
if (str($value)->contains('(')) {
$status = str($value)->before('(')->trim()->value();
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
} else if (str($value)->contains(':')) {
$status = str($value)->before(':')->trim()->value();
$health = str($value)->after(':')->trim()->value() ?? 'unhealthy';
} else {
$status = $value;
$health = 'unhealthy';
}
return "$status:$health";
},
get: function ($value) {
if (str($value)->contains('(')) {
$status = str($value)->before('(')->trim()->value();
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
} else if (str($value)->contains(':')) {
$status = str($value)->before(':')->trim()->value();
$health = str($value)->after(':')->trim()->value() ?? 'unhealthy';
} else {
$status = $value;
$health = 'unhealthy';
}
return "$status:$health";
},
);
}
public function tags()
{
return $this->morphToMany(Tag::class, 'taggable');
}
public function project()
{
return data_get($this, 'environment.project');
}
public function team()
{
return data_get($this, 'environment.project.team');
}
public function link()
{
if (data_get($this, 'environment.project.uuid')) {
return route('project.database.configuration', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'database_uuid' => data_get($this, 'uuid')
]);
}
return null;
}
public function isLogDrainEnabled()
{
return data_get($this, 'is_log_drain_enabled', false);
}
public function portsMappings(): Attribute
{
return Attribute::make(
set: fn ($value) => $value === "" ? null : $value,
);
}
public function portsMappingsArray(): Attribute
{
return Attribute::make(
get: fn () => is_null($this->ports_mappings)
? []
: explode(',', $this->ports_mappings),
);
}
public function type(): string
{
return 'standalone-keydb';
}
public function get_db_url(bool $useInternal = false): string
{
if ($this->is_public && !$useInternal) {
return "redis://{$this->keydb_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
} else {
return "redis://{$this->keydb_password}@{$this->uuid}:6379/0";
}
}
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

@ -43,6 +43,45 @@ protected static function booted()
$database->tags()->detach(); $database->tags()->detach();
}); });
} }
public function isConfigurationChanged(bool $save = false)
{
$newConfigHash = $this->image . $this->ports_mappings . $this->mariadb_conf;
$newConfigHash .= json_encode($this->environment_variables()->get('updated_at'));
$newConfigHash = md5($newConfigHash);
$oldConfigHash = data_get($this, 'config_hash');
if ($oldConfigHash === null) {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
if ($oldConfigHash === $newConfigHash) {
return false;
} else {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
}
public function isExited()
{
return (bool) str($this->status)->startsWith('exited');
}
public function workdir()
{
return database_configuration_dir() . "/{$this->uuid}";
}
public function delete_configurations()
{
$server = data_get($this, 'destination.server');
$workdir = $this->workdir();
if (str($workdir)->endsWith($this->uuid)) {
instant_remote_process(["rm -rf " . $this->workdir()], $server, false);
}
}
public function realStatus() public function realStatus()
{ {
return $this->getRawOriginal('status'); return $this->getRawOriginal('status');

View File

@ -46,6 +46,45 @@ protected static function booted()
$database->tags()->detach(); $database->tags()->detach();
}); });
} }
public function isConfigurationChanged(bool $save = false)
{
$newConfigHash = $this->image . $this->ports_mappings . $this->mongo_conf;
$newConfigHash .= json_encode($this->environment_variables()->get('updated_at'));
$newConfigHash = md5($newConfigHash);
$oldConfigHash = data_get($this, 'config_hash');
if ($oldConfigHash === null) {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
if ($oldConfigHash === $newConfigHash) {
return false;
} else {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
}
public function isExited()
{
return (bool) str($this->status)->startsWith('exited');
}
public function workdir()
{
return database_configuration_dir() . "/{$this->uuid}";
}
public function delete_configurations()
{
$server = data_get($this, 'destination.server');
$workdir = $this->workdir();
if (str($workdir)->endsWith($this->uuid)) {
instant_remote_process(["rm -rf " . $this->workdir()], $server, false);
}
}
public function realStatus() public function realStatus()
{ {
return $this->getRawOriginal('status'); return $this->getRawOriginal('status');

View File

@ -43,6 +43,45 @@ protected static function booted()
$database->tags()->detach(); $database->tags()->detach();
}); });
} }
public function isConfigurationChanged(bool $save = false)
{
$newConfigHash = $this->image . $this->ports_mappings . $this->mysql_conf;
$newConfigHash .= json_encode($this->environment_variables()->get('updated_at'));
$newConfigHash = md5($newConfigHash);
$oldConfigHash = data_get($this, 'config_hash');
if ($oldConfigHash === null) {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
if ($oldConfigHash === $newConfigHash) {
return false;
} else {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
}
public function isExited()
{
return (bool) str($this->status)->startsWith('exited');
}
public function workdir()
{
return database_configuration_dir() . "/{$this->uuid}";
}
public function delete_configurations()
{
$server = data_get($this, 'destination.server');
$workdir = $this->workdir();
if (str($workdir)->endsWith($this->uuid)) {
instant_remote_process(["rm -rf " . $this->workdir()], $server, false);
}
}
public function realStatus() public function realStatus()
{ {
return $this->getRawOriginal('status'); return $this->getRawOriginal('status');

View File

@ -43,6 +43,45 @@ protected static function booted()
$database->tags()->detach(); $database->tags()->detach();
}); });
} }
public function workdir()
{
return database_configuration_dir() . "/{$this->uuid}";
}
public function delete_configurations()
{
$server = data_get($this, 'destination.server');
$workdir = $this->workdir();
if (str($workdir)->endsWith($this->uuid)) {
instant_remote_process(["rm -rf " . $this->workdir()], $server, false);
}
}
public function isConfigurationChanged(bool $save = false)
{
$newConfigHash = $this->image . $this->ports_mappings . $this->postgres_initdb_args . $this->postgres_host_auth_method;
$newConfigHash .= json_encode($this->environment_variables()->get('updated_at'));
$newConfigHash = md5($newConfigHash);
$oldConfigHash = data_get($this, 'config_hash');
if ($oldConfigHash === null) {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
if ($oldConfigHash === $newConfigHash) {
return false;
} else {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
}
public function isExited()
{
return (bool) str($this->status)->startsWith('exited');
}
public function realStatus() public function realStatus()
{ {
return $this->getRawOriginal('status'); return $this->getRawOriginal('status');

View File

@ -38,6 +38,45 @@ protected static function booted()
$database->tags()->detach(); $database->tags()->detach();
}); });
} }
public function isConfigurationChanged(bool $save = false)
{
$newConfigHash = $this->image . $this->ports_mappings . $this->redis_conf;
$newConfigHash .= json_encode($this->environment_variables()->get('updated_at'));
$newConfigHash = md5($newConfigHash);
$oldConfigHash = data_get($this, 'config_hash');
if ($oldConfigHash === null) {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
if ($oldConfigHash === $newConfigHash) {
return false;
} else {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
}
public function isExited()
{
return (bool) str($this->status)->startsWith('exited');
}
public function workdir()
{
return database_configuration_dir() . "/{$this->uuid}";
}
public function delete_configurations()
{
$server = data_get($this, 'destination.server');
$workdir = $this->workdir();
if (str($workdir)->endsWith($this->uuid)) {
instant_remote_process(["rm -rf " . $this->workdir()], $server, false);
}
}
public function realStatus() public function realStatus()
{ {
return $this->getRawOriginal('status'); return $this->getRawOriginal('status');

View File

@ -20,6 +20,18 @@ public function redis()
{ {
return $this->morphMany(StandaloneRedis::class, 'destination'); return $this->morphMany(StandaloneRedis::class, 'destination');
} }
public function keydbs()
{
return $this->morphMany(StandaloneKeydb::class, 'destination');
}
public function dragonflies()
{
return $this->morphMany(StandaloneDragonfly::class, 'destination');
}
public function clickhouses()
{
return $this->morphMany(StandaloneClickhouse::class, 'destination');
}
public function mongodbs() public function mongodbs()
{ {
return $this->morphMany(StandaloneMongodb::class, 'destination'); return $this->morphMany(StandaloneMongodb::class, 'destination');
@ -50,7 +62,10 @@ public function databases()
$mongodbs = $this->mongodbs; $mongodbs = $this->mongodbs;
$mysqls = $this->mysqls; $mysqls = $this->mysqls;
$mariadbs = $this->mariadbs; $mariadbs = $this->mariadbs;
return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs); $keydbs = $this->keydbs;
$dragonflies = $this->dragonflies;
$clickhouses = $this->clickhouses;
return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs)->concat($keydbs)->concat($dragonflies)->concat($clickhouses);
} }
public function attachedTo() public function attachedTo()

View File

@ -22,6 +22,7 @@ public function __construct(
public bool $required = false, public bool $required = false,
public bool $disabled = false, public bool $disabled = false,
public bool $readonly = false, public bool $readonly = false,
public bool $allowTab = false,
public ?string $helper = null, public ?string $helper = null,
public bool $realtimeValidation = false, public bool $realtimeValidation = false,
public bool $allowToPeak = true, public bool $allowToPeak = true,

View File

@ -1,7 +1,7 @@
<?php <?php
const REDACTED = '<REDACTED>'; const REDACTED = '<REDACTED>';
const DATABASE_TYPES = ['postgresql', 'redis', 'mongodb', 'mysql', 'mariadb']; const DATABASE_TYPES = ['postgresql', 'redis', 'mongodb', 'mysql', 'mariadb', 'keydb', 'dragonfly', 'clickhouse'];
const VALID_CRON_STRINGS = [ const VALID_CRON_STRINGS = [
'every_minute' => '* * * * *', 'every_minute' => '* * * * *',
'hourly' => '0 * * * *', 'hourly' => '0 * * * *',

View File

@ -1,7 +1,10 @@
<?php <?php
use App\Models\Server; use App\Models\Server;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDocker; use App\Models\StandaloneDocker;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
use App\Models\StandaloneMariadb; use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb; use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql; use App\Models\StandaloneMysql;
@ -90,6 +93,49 @@ function create_standalone_mariadb($environment_id, $destination_uuid): Standalo
'destination_type' => $destination->getMorphClass(), 'destination_type' => $destination->getMorphClass(),
]); ]);
} }
function create_standalone_keydb($environment_id, $destination_uuid): StandaloneKeydb
{
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
if (!$destination) {
throw new Exception('Destination not found');
}
return StandaloneKeydb::create([
'name' => generate_database_name('keydb'),
'keydb_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
'environment_id' => $environment_id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
]);
}
function create_standalone_dragonfly($environment_id, $destination_uuid): StandaloneDragonfly
{
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
if (!$destination) {
throw new Exception('Destination not found');
}
return StandaloneDragonfly::create([
'name' => generate_database_name('dragonfly'),
'dragonfly_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
'environment_id' => $environment_id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
]);
}
function create_standalone_clickhouse($environment_id, $destination_uuid): StandaloneClickhouse
{
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
if (!$destination) {
throw new Exception('Destination not found');
}
return StandaloneClickhouse::create([
'name' => generate_database_name('clickhouse'),
'clickhouse_admin_password' => \Illuminate\Support\Str::password(length: 64, 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.

View File

@ -582,7 +582,7 @@ function validateComposeFile(string $compose, int $server_id): string|Throwable
function escapeEnvVariables($value) function escapeEnvVariables($value)
{ {
$search = array("\\", "\r", "\t", "\x0", '"', "'", "$"); $search = array("\\", "\r", "\t", "\x0", '"', "'");
$replace = array("\\\\", "\\r", "\\t", "\\0", '\"', "\'", "$$"); $replace = array("\\\\", "\\r", "\\t", "\\0", '\"', "\'");
return str_replace($search, $replace, $value); return str_replace($search, $replace, $value);
} }

View File

@ -11,6 +11,9 @@
use App\Models\Service; use App\Models\Service;
use App\Models\ServiceApplication; use App\Models\ServiceApplication;
use App\Models\ServiceDatabase; use App\Models\ServiceDatabase;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
use App\Models\StandaloneMariadb; use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb; use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql; use App\Models\StandaloneMysql;
@ -463,15 +466,14 @@ function getServiceTemplates()
function getResourceByUuid(string $uuid, ?int $teamId = null) function getResourceByUuid(string $uuid, ?int $teamId = null)
{ {
$resource = queryResourcesByUuid($uuid); if (is_null($teamId)) {
if (!is_null($teamId)) {
if (!is_null($resource) && $resource->environment->project->team_id === $teamId) {
return $resource;
}
return null; return null;
} else { }
$resource = queryResourcesByUuid($uuid);
if (!is_null($resource) && $resource->environment->project->team_id === $teamId) {
return $resource; return $resource;
} }
return null;
} }
function queryResourcesByUuid(string $uuid) function queryResourcesByUuid(string $uuid)
{ {
@ -490,6 +492,12 @@ function queryResourcesByUuid(string $uuid)
if ($mysql) return $mysql; if ($mysql) return $mysql;
$mariadb = StandaloneMariadb::whereUuid($uuid)->first(); $mariadb = StandaloneMariadb::whereUuid($uuid)->first();
if ($mariadb) return $mariadb; if ($mariadb) return $mariadb;
$keydb = StandaloneKeydb::whereUuid($uuid)->first();
if ($keydb) return $keydb;
$dragonfly = StandaloneDragonfly::whereUuid($uuid)->first();
if ($dragonfly) return $dragonfly;
$clickhouse = StandaloneClickhouse::whereUuid($uuid)->first();
if ($clickhouse) return $clickhouse;
return $resource; return $resource;
} }
function generatTagDeployWebhook($tag_name) function generatTagDeployWebhook($tag_name)

View File

@ -27,6 +27,7 @@
"lorisleiva/laravel-actions": "^2.7", "lorisleiva/laravel-actions": "^2.7",
"nubs/random-name-generator": "^2.2", "nubs/random-name-generator": "^2.2",
"phpseclib/phpseclib": "~3.0", "phpseclib/phpseclib": "~3.0",
"pion/laravel-chunk-upload": "^1.5",
"poliander/cron": "^3.0", "poliander/cron": "^3.0",
"purplepixie/phpdns": "^2.1", "purplepixie/phpdns": "^2.1",
"pusher/pusher-php-server": "^7.2", "pusher/pusher-php-server": "^7.2",

68
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "e095b8a9eb22df2943cbc3e9649ff9e8", "content-hash": "e6fd1d5c5183226a78df717b52343393",
"packages": [ "packages": [
{ {
"name": "amphp/amp", "name": "amphp/amp",
@ -6370,6 +6370,72 @@
}, },
"time": "2021-10-28T11:13:42+00:00" "time": "2021-10-28T11:13:42+00:00"
}, },
{
"name": "pion/laravel-chunk-upload",
"version": "v1.5.4",
"source": {
"type": "git",
"url": "https://github.com/pionl/laravel-chunk-upload.git",
"reference": "cfbc4292ddcace51308a4f2f446d310aa04e6133"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pionl/laravel-chunk-upload/zipball/cfbc4292ddcace51308a4f2f446d310aa04e6133",
"reference": "cfbc4292ddcace51308a4f2f446d310aa04e6133",
"shasum": ""
},
"require": {
"illuminate/console": "5.2 - 5.8 | ^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0",
"illuminate/filesystem": "5.2 - 5.8 | ^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0",
"illuminate/http": "5.2 - 5.8 | ^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0",
"illuminate/support": "5.2 - 5.8 | ^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.16.0 | ^3.52.0",
"mockery/mockery": "^1.1.0 | ^1.3.0 | ^1.6.0",
"overtrue/phplint": "^1.1 | ^2.0 | ^9.1",
"phpunit/phpunit": "5.7 | 6.0 | 7.0 | 7.5 | 8.4 | ^8.5 | ^9.3 | ^10.0 | ^11.0"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Pion\\Laravel\\ChunkUpload\\Providers\\ChunkUploadServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Pion\\Laravel\\ChunkUpload\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Martin Kluska",
"email": "martin@kluska.cz"
}
],
"description": "Service for chunked upload with several js providers",
"support": {
"issues": "https://github.com/pionl/laravel-chunk-upload/issues",
"source": "https://github.com/pionl/laravel-chunk-upload/tree/v1.5.4"
},
"funding": [
{
"url": "https://revolut.me/martinpv7n",
"type": "custom"
},
{
"url": "https://github.com/pionl",
"type": "github"
}
],
"time": "2024-03-25T15:50:07+00:00"
},
{ {
"name": "poliander/cron", "name": "poliander/cron",
"version": "3.1.0", "version": "3.1.0",

44
config/chunk-upload.php Normal file
View File

@ -0,0 +1,44 @@
<?php
/**
* @see https://github.com/pionl/laravel-chunk-upload
*/
return [
/*
* The storage config
*/
'storage' => [
/*
* Returns the folder name of the chunks. The location is in storage/app/{folder_name}
*/
'chunks' => 'chunks',
'disk' => 'local',
],
'clear' => [
/*
* How old chunks we should delete
*/
'timestamp' => '-1 HOURS',
'schedule' => [
'enabled' => false,
'cron' => '25 * * * *', // run every hour on the 25th minute
],
],
'chunk' => [
// setup for the chunk naming setup to ensure same name upload at same time
'name' => [
'use' => [
'session' => true, // should the chunk name use the session id? The uploader must send cookie!,
'browser' => false, // instead of session we can use the ip and browser?
],
],
],
'handlers' => [
// A list of handlers/providers that will be appended to existing list of handlers
'custom' => [],
// Overrides the list of handlers - use only what you really want
'override' => [
// \Pion\Laravel\ChunkUpload\Handler\DropZoneUploadHandler::class
],
],
];

View File

@ -7,7 +7,7 @@
// 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.255', 'release' => '4.0.0-beta.256',
// 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.255'; return '4.0.0-beta.256';

View File

@ -0,0 +1,64 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('standalone_keydbs', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->string('name');
$table->string('description')->nullable();
$table->text('keydb_password');
$table->longText('keydb_conf')->nullable();
$table->boolean('is_log_drain_enabled')->default(false);
$table->boolean('is_include_timestamps')->default(false);
$table->softDeletes();
$table->string('status')->default('exited');
$table->string('image')->default('eqalpha/keydb:latest');
$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(null);
$table->integer('limits_cpu_shares')->default(1024);
$table->timestamp('started_at')->nullable();
$table->morphs('destination');
$table->foreignId('environment_id')->nullable();
$table->timestamps();
});
Schema::table('environment_variables', function (Blueprint $table) {
$table->foreignId('standalone_keydb_id')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('standalone_keydbs');
Schema::table('environment_variables', function (Blueprint $table) {
$table->dropColumn('standalone_keydb_id');
});
}
};

View File

@ -0,0 +1,63 @@
<?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_dragonflies', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->string('name');
$table->string('description')->nullable();
$table->text('dragonfly_password');
$table->boolean('is_log_drain_enabled')->default(false);
$table->boolean('is_include_timestamps')->default(false);
$table->softDeletes();
$table->string('status')->default('exited');
$table->string('image')->default('docker.dragonflydb.io/dragonflydb/dragonfly');
$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(null);
$table->integer('limits_cpu_shares')->default(1024);
$table->timestamp('started_at')->nullable();
$table->morphs('destination');
$table->foreignId('environment_id')->nullable();
$table->timestamps();
});
Schema::table('environment_variables', function (Blueprint $table) {
$table->foreignId('standalone_dragonfly_id')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('standalone_dragonflies');
Schema::table('environment_variables', function (Blueprint $table) {
$table->dropColumn('standalone_dragonfly_id');
});
}
};

View File

@ -0,0 +1,64 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('standalone_clickhouses', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->string('name');
$table->string('description')->nullable();
$table->string('clickhouse_admin_user')->default('default');
$table->text('clickhouse_admin_password');
$table->boolean('is_log_drain_enabled')->default(false);
$table->boolean('is_include_timestamps')->default(false);
$table->softDeletes();
$table->string('status')->default('exited');
$table->string('image')->default('bitnami/clickhouse');
$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(null);
$table->integer('limits_cpu_shares')->default(1024);
$table->timestamp('started_at')->nullable();
$table->morphs('destination');
$table->foreignId('environment_id')->nullable();
$table->timestamps();
});
Schema::table('environment_variables', function (Blueprint $table) {
$table->foreignId('standalone_clickhouse_id')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('standalone_clickhouses');
Schema::table('environment_variables', function (Blueprint $table) {
$table->dropColumn('standalone_clickhouse_id');
});
}
};

View File

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

View File

@ -0,0 +1,76 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('standalone_postgresqls', function (Blueprint $table) {
$table->string('config_hash')->nullable();
});
Schema::table('standalone_redis', function (Blueprint $table) {
$table->string('config_hash')->nullable();
});
Schema::table('standalone_mysqls', function (Blueprint $table) {
$table->string('config_hash')->nullable();
});
Schema::table('standalone_mariadbs', function (Blueprint $table) {
$table->string('config_hash')->nullable();
});
Schema::table('standalone_mongodbs', function (Blueprint $table) {
$table->string('config_hash')->nullable();
});
Schema::table('standalone_keydbs', function (Blueprint $table) {
$table->string('config_hash')->nullable();
});
Schema::table('standalone_dragonflies', function (Blueprint $table) {
$table->string('config_hash')->nullable();
});
Schema::table('standalone_clickhouses', function (Blueprint $table) {
$table->string('config_hash')->nullable();
});
Schema::table('services', function (Blueprint $table) {
$table->string('config_hash')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('standalone_postgresqls', function (Blueprint $table) {
$table->dropColumn('config_hash');
});
Schema::table('standalone_redis', function (Blueprint $table) {
$table->dropColumn('config_hash');
});
Schema::table('standalone_mysqls', function (Blueprint $table) {
$table->dropColumn('config_hash');
});
Schema::table('standalone_mariadbs', function (Blueprint $table) {
$table->dropColumn('config_hash');
});
Schema::table('standalone_mongodbs', function (Blueprint $table) {
$table->dropColumn('config_hash');
});
Schema::table('standalone_keydbs', function (Blueprint $table) {
$table->dropColumn('config_hash');
});
Schema::table('standalone_dragonflies', function (Blueprint $table) {
$table->dropColumn('config_hash');
});
Schema::table('standalone_clickhouses', function (Blueprint $table) {
$table->dropColumn('config_hash');
});
Schema::table('services', function (Blueprint $table) {
$table->dropColumn('config_hash');
});
}
};

View File

@ -1,2 +1,5 @@
#!/command/execlineb -P #!/command/execlineb -P
su - webuser -c "php /var/www/html/artisan horizon" foreground {
s6-sleep 5
su - webuser -c "php /var/www/html/artisan start:horizon"
}

View File

@ -1,2 +1,5 @@
#!/command/execlineb -P #!/command/execlineb -P
su - webuser -c "php /var/www/html/artisan schedule:work" foreground {
s6-sleep 5
su - webuser -c "php /var/www/html/artisan start:scheduler"
}

View File

@ -289,3 +289,7 @@ .fullscreen {
.toast { .toast {
z-index: 1; z-index: 1;
} }
.dz-button {
@apply w-full p-4 py-10 my-4 font-bold bg-white border dark:border-coolgray-400 dark:text-white dark:bg-transparent hover:dark:bg-coolgray-400;
}

View File

@ -1,41 +1,41 @@
<x-layout-simple> <x-layout-simple>
<div class="min-h-screen hero"> <section class="bg-gray-50 dark:bg-base">
<div> <div class="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
<div class="flex flex-col items-center "> <a class="flex items-center text-5xl font-extrabold tracking-tight text-gray-900 dark:text-white">
<a href="{{ route('dashboard') }}"> Coolify
<div class="text-5xl font-bold tracking-tight text-center dark:text-white">Coolify</div> </a>
</a> <div class="flex items-center justify-center pb-6 text-center">
</div>
<div class="flex items-center justify-center pb-4 text-center">
{{ __('auth.reset_password') }} {{ __('auth.reset_password') }}
</div> </div>
<div> <div class="w-full bg-white shadow md:mt-0 sm:max-w-md xl:p-0 dark:bg-base ">
<form action="/reset-password" method="POST" class="flex flex-col gap-2"> <div class="p-6 space-y-4 md:space-y-6 sm:p-8">
@csrf <form action="/reset-password" method="POST" class="flex flex-col gap-2">
<input hidden id="token" name="token" value="{{ request()->route('token') }}"> @csrf
<input hidden value="{{ request()->query('email') }}" type="email" name="email" <input hidden id="token" name="token" value="{{ request()->route('token') }}">
label="{{ __('input.email') }}" /> <input hidden value="{{ request()->query('email') }}" type="email" name="email"
<div class="flex flex-col gap-2"> label="{{ __('input.email') }}" />
<x-forms.input required type="password" id="password" name="password" <div class="flex flex-col gap-2">
label="{{ __('input.password') }}" autofocus /> <x-forms.input required type="password" id="password" name="password"
<x-forms.input required type="password" id="password_confirmation" name="password_confirmation" label="{{ __('input.password') }}" autofocus />
label="{{ __('input.password.again') }}" /> <x-forms.input required type="password" id="password_confirmation"
</div> name="password_confirmation" label="{{ __('input.password.again') }}" />
<x-forms.button type="submit">{{ __('auth.reset_password') }}</x-forms.button> </div>
</form> <x-forms.button type="submit">{{ __('auth.reset_password') }}</x-forms.button>
@if ($errors->any()) </form>
<div class="text-xs text-center text-error"> @if ($errors->any())
@foreach ($errors->all() as $error) <div class="text-xs text-center text-error">
<p>{{ $error }}</p> @foreach ($errors->all() as $error)
@endforeach <p>{{ $error }}</p>
</div> @endforeach
@endif </div>
@if (session('status')) @endif
<div class="mb-4 font-medium text-green-600"> @if (session('status'))
{{ session('status') }} <div class="mb-4 font-medium text-green-600">
</div> {{ session('status') }}
@endif </div>
@endif
</div>
</div> </div>
</div> </div>
</div> </section>
</x-layout-simple> </x-layout-simple>

View File

@ -5,11 +5,14 @@
@endisset @endisset
@isset($confirmAction) @isset($confirmAction)
x-on:{{ explode('(', $confirmAction)[0] }}.window="$wire.{{ explode('(', $confirmAction)[0] }}" x-on:{{ explode('(', $confirmAction)[0] }}.window="$wire.{{ explode('(', $confirmAction)[0] }}"
@endisset @endisset>
>
{{ $slot }} {{ $slot }}
@if ($attributes->whereStartsWith('wire:click')->first()) @if ($attributes->whereStartsWith('wire:click')->first())
<x-loading-on-button wire:target="{{ $attributes->whereStartsWith('wire:click')->first() }}" wire:loading.delay /> <x-loading-on-button wire:target="{{ $attributes->whereStartsWith('wire:click')->first() }}"
wire:loading.delay />
@elseif($attributes->whereStartsWith('wire:target')->first())
<x-loading-on-button wire:target="{{ $attributes->whereStartsWith('wire:target')->first() }}"
wire:loading.delay />
@endif @endif
</button> </button>

View File

@ -1,3 +1,18 @@
<script>
function handleKeydown(e) {
if (e.keyCode === 9) {
e.preventDefault();
e.target.setRangeText(
'\t',
e.target.selectionStart,
e.target.selectionStart,
'end'
);
}
}
</script>
<div class="flex-1 form-control"> <div class="flex-1 form-control">
@if ($label) @if ($label)
<label class="flex items-center gap-1 mb-1 text-sm font-medium">{{ $label }} <label class="flex items-center gap-1 mb-1 text-sm font-medium">{{ $label }}
@ -41,7 +56,7 @@ class="absolute inset-y-0 right-0 flex items-center h-6 pt-2 pr-2 cursor-pointer
</div> </div>
@else @else
<textarea placeholder="{{ $placeholder }}" {{ $attributes->merge(['class' => $defaultClass]) }} <textarea @keydown.tab="{{ $allowTab }} && handleKeydown" placeholder="{{ $placeholder }}" {{ $attributes->merge(['class' => $defaultClass]) }}
@if ($realtimeValidation) wire:model.debounce.200ms="{{ $id }}" @if ($realtimeValidation) wire:model.debounce.200ms="{{ $id }}"
@else @else
wire:model={{ $value ?? $id }} wire:model={{ $value ?? $id }}

View File

@ -1,9 +1,14 @@
<div {{ $attributes->merge(['class' => "group"]) }}> <div {{ $attributes->merge(['class' => 'group']) }}>
<div class="info-helper"> <div class="info-helper">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-4 h-4 stroke-current"> @isset($icon)
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" {{ $icon }}
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path> @else
</svg> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-4 h-4 stroke-current">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
@endisset
</div> </div>
<div class="info-helper-popup"> <div class="info-helper-popup">
<div class="p-4"> <div class="p-4">

View File

@ -0,0 +1,28 @@
@props(['title' => 'Default title', 'description' => 'Default Description', 'buttonText' => 'Default Button Text'])
<div x-data="{
bannerVisible: true,
bannerVisibleAfter: 100
}" x-show="bannerVisible" x-transition:enter="transition ease-out duration-100"
x-transition:enter-start="translate-y-full" x-transition:enter-end="translate-y-0"
x-transition:leave="transition ease-in duration-300" x-transition:leave-start="translate-y-0"
x-transition:leave-end="translate-y-full" x-init="setTimeout(() => { bannerVisible = true }, bannerVisibleAfter);"
class="fixed bottom-0 right-0 h-auto duration-300 ease-out px-5 pb-5 max-w-[46rem] z-[999]"
x-cloak>
<div
class="flex flex-col items-center justify-between w-full h-full max-w-4xl p-6 mx-auto bg-white border shadow-lg lg:border-t dark:border-coolgray-300 dark:bg-coolgray-100 lg:p-8 lg:flex-row sm:rounded">
<div
class="flex flex-col items-start h-full pb-0 text-xs lg:items-center lg:flex-row lg:pr-6 lg:space-x-5 dark:text-neutral-300 ">
@if (isset($icon))
{{ $icon }}
@endif
<div class="pt-0">
<h4 class="w-full mb-1 text-base font-bold leading-none -translate-y-1 lg:text-xl text-neutral-900 dark:text-white">
{{ $title }}
</h4>
<p class="">{{ $description }}</span></p>
</div>
</div>
</div>
</div>

View File

@ -9,7 +9,17 @@
{{ str($status)->before(':')->headline() }} {{ str($status)->before(':')->headline() }}
</div> </div>
@if (!str($status)->startsWith('Proxy') && !str($status)->contains('(')) @if (!str($status)->startsWith('Proxy') && !str($status)->contains('('))
<div class="text-xs {{ str($status)->contains('unhealthy') ? 'dark:text-warning' : 'text-success' }}">({{ str($status)->after(':') }})</div> @if (str($status)->contains('unhealthy'))
<x-helper helper="Unhealthy state. <span class='dark:text-warning text-coollabs'>This doesn't mean that the resource is malfunctioning.</span><br><br>- If the resource is accessible, it indicates that no health check is configured - it is not mandatory.<br>- If the resource is not accessible (returning 404 or 503), it may indicate that a health check is needed and has not passed. <span class='dark:text-warning text-coollabs'>Your action is required.</span>" >
<x-slot:icon>
<svg class="hidden w-4 h-4 dark:text-warning lg:block" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" d="M240.26 186.1L152.81 34.23a28.74 28.74 0 0 0-49.62 0L15.74 186.1a27.45 27.45 0 0 0 0 27.71A28.31 28.31 0 0 0 40.55 228h174.9a28.31 28.31 0 0 0 24.79-14.19a27.45 27.45 0 0 0 .02-27.71m-20.8 15.7a4.46 4.46 0 0 1-4 2.2H40.55a4.46 4.46 0 0 1-4-2.2a3.56 3.56 0 0 1 0-3.73L124 46.2a4.77 4.77 0 0 1 8 0l87.44 151.87a3.56 3.56 0 0 1 .02 3.73M116 136v-32a12 12 0 0 1 24 0v32a12 12 0 0 1-24 0m28 40a16 16 0 1 1-16-16a16 16 0 0 1 16 16"></path>
</svg>
</x-slot:icon>
</x-helper>
{{-- @else
<div class="text-xs dark:text-success">({{ str($status)->after(':') }})</div> --}}
@endif
@endif @endif
</span> </span>
</div> </div>

View File

@ -1,17 +1,19 @@
<div class="min-h-screen hero"> <section class="bg-gray-50 dark:bg-base">
<div class="w-96 min-w-fit"> <div class="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
<div class="flex flex-col items-center"> <a class="flex items-center mb-6 text-5xl font-extrabold tracking-tight text-gray-900 dark:text-white">
<a href="{{ route('dashboard') }}"> Coolify
<div class="text-5xl font-bold tracking-tight text-center dark:text-white">Coolify</div> </a>
</a> <div class="w-full bg-white shadow md:mt-0 sm:max-w-md xl:p-0 dark:bg-base ">
<div class="p-6 space-y-4 md:space-y-6 sm:p-8">
<form class="flex flex-col gap-2" wire:submit='submit'>
<x-forms.input id="email" type="email" placeholder="Email" readonly label="Email" />
<x-forms.input id="password" type="password" placeholder="New Password" label="New Password"
required />
<x-forms.input id="password_confirmation" type="password" placeholder="Confirm New Password"
label="Confirm New Password" required />
<x-forms.button type="submit">Reset Password</x-forms.button>
</form>
</div>
</div> </div>
<form class="flex flex-col gap-2" wire:submit='submit'>
<x-forms.input id="email" type="email" placeholder="Email" readonly label="Email" />
<x-forms.input id="password" type="password" placeholder="New Password" label="New Password" required />
<x-forms.input id="password_confirmation" type="password" placeholder="Confirm New Password"
label="Confirm New Password" required />
<x-forms.button type="submit">Reset Password</x-forms.button>
</form>
</div> </div>
</div> </section>

View File

@ -1,5 +1,6 @@
<div> <div>
<h1>Configuration</h1> <h1>Configuration</h1>
<livewire:project.shared.configuration-checker :resource="$application" />
<livewire:project.application.heading :application="$application" /> <livewire:project.application.heading :application="$application" />
<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-2 xl:w-48"> <div class="flex flex-col gap-2 xl:w-48">
@ -27,7 +28,7 @@
<a class="menu-item" :class="activeTab === 'source' && 'menu-item-active'" <a class="menu-item" :class="activeTab === 'source' && 'menu-item-active'"
@click.prevent="activeTab = 'source'; window.location.hash = 'source'" href="#">Source</a> @click.prevent="activeTab = 'source'; window.location.hash = 'source'" href="#">Source</a>
@endif @endif
<a class="menu-item" :class="activeTab === 'servers' && 'menu-item-active'" class="flex items-center gap-2" <a class="menu-item" :class="activeTab === 'servers' && 'menu-item-active'" class="flex items-center gap-2"
@click.prevent="activeTab = 'servers'; window.location.hash = 'servers'" href="#">Servers @click.prevent="activeTab = 'servers'; window.location.hash = 'servers'" href="#">Servers
@if (str($application->status)->contains('degraded')) @if (str($application->status)->contains('degraded'))
<span title="Some servers are unavailable"> <span title="Some servers are unavailable">

View File

@ -1,5 +1,6 @@
<div> <div>
<h1>Deployments</h1> <h1>Deployments</h1>
<livewire:project.shared.configuration-checker :resource="$application" />
<livewire:project.application.heading :application="$application" /> <livewire:project.application.heading :application="$application" />
{{-- <livewire:project.application.deployment.show :application="$application" :deployments="$deployments" :deployments_count="$deployments_count" /> --}} {{-- <livewire:project.application.deployment.show :application="$application" :deployments="$deployments" :deployments_count="$deployments_count" /> --}}
<div class="flex flex-col gap-2 pb-10" <div class="flex flex-col gap-2 pb-10"

View File

@ -1,5 +1,6 @@
<div> <div>
<h1 class="py-0">Deployment</h1> <h1 class="py-0">Deployment</h1>
<livewire:project.shared.configuration-checker :resource="$application" />
<livewire:project.application.heading :application="$application" /> <livewire:project.application.heading :application="$application" />
<div class="pt-4" x-data="{ <div class="pt-4" x-data="{
fullscreen: false, fullscreen: false,
@ -85,10 +86,10 @@ class="fixed top-4 right-16" x-on:click="toggleScroll"><svg class="icon" viewBox
@if (decode_remote_command_output($application_deployment_queue)->count() > 0) @if (decode_remote_command_output($application_deployment_queue)->count() > 0)
@foreach (decode_remote_command_output($application_deployment_queue) as $line) @foreach (decode_remote_command_output($application_deployment_queue) as $line)
<span @class([ <span @class([
'dark:text-warning' => $line['hidden'], 'dark:text-warning whitespace-pre-line' => $line['hidden'],
'text-red-500 font-bold' => $line['type'] == 'stderr', 'text-red-500 font-bold whitespace-pre-line' => $line['type'] == 'stderr',
])>[{{ $line['timestamp'] }}] @if ($line['hidden']) ])>[{{ $line['timestamp'] }}] @if ($line['hidden'])
<br>COMMAND: {{ $line['command'] }}<br>OUTPUT : <br><br>COMMAND: {{ $line['command'] }}<br><br>OUTPUT :
@endif @if (str($line['output'])->contains('http://') || str($line['output'])->contains('https://')) @endif @if (str($line['output'])->contains('http://') || str($line['output'])->contains('https://'))
@php @php
$line['output'] = preg_replace( $line['output'] = preg_replace(
@ -100,6 +101,7 @@ class="fixed top-4 right-16" x-on:click="toggleScroll"><svg class="icon" viewBox
@else @else
{{ $line['output'] }} {{ $line['output'] }}
@endif @endif
<br>
</span> </span>
@endforeach @endforeach
@else @else

View File

@ -1,30 +1,23 @@
<div> <div x-data="{ initLoadingCompose: $wire.entangle('initLoadingCompose') }">
<form wire:submit='submit' class="flex flex-col"> <form wire:submit='submit' class="flex flex-col pb-32">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h2>General</h2> <h2>General</h2>
<x-forms.button type="submit"> <x-forms.button type="submit">
Save Save
</x-forms.button> </x-forms.button>
@if ($isConfigurationChanged && !is_null($application->config_hash) && !$application->isExited())
<div title="Configuration not applied to the running application. You need to redeploy.">
<svg class="w-6 h-6 dark:text-warning" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor"
d="M240.26 186.1L152.81 34.23a28.74 28.74 0 0 0-49.62 0L15.74 186.1a27.45 27.45 0 0 0 0 27.71A28.31 28.31 0 0 0 40.55 228h174.9a28.31 28.31 0 0 0 24.79-14.19a27.45 27.45 0 0 0 .02-27.71m-20.8 15.7a4.46 4.46 0 0 1-4 2.2H40.55a4.46 4.46 0 0 1-4-2.2a3.56 3.56 0 0 1 0-3.73L124 46.2a4.77 4.77 0 0 1 8 0l87.44 151.87a3.56 3.56 0 0 1 .02 3.73M116 136v-32a12 12 0 0 1 24 0v32a12 12 0 0 1-24 0m28 40a16 16 0 1 1-16-16a16 16 0 0 1 16 16" />
</svg>
</div>
@endif
</div> </div>
<div>General configuration for your application.</div> <div>General configuration for your application.</div>
<div class="flex flex-col gap-2 py-4"> <div class="flex flex-col gap-2 py-4">
<div class="flex flex-col items-end gap-2 xl:flex-row"> <div class="flex flex-col items-end gap-2 xl:flex-row">
<x-forms.input id="application.name" label="Name" required /> <x-forms.input x-bind:disabled="initLoadingCompose" id="application.name" label="Name" required />
<x-forms.input id="application.description" label="Description" /> <x-forms.input x-bind:disabled="initLoadingCompose" id="application.description" label="Description" />
</div> </div>
@if (!$application->dockerfile && $application->build_pack !== 'dockerimage') @if (!$application->dockerfile && $application->build_pack !== 'dockerimage')
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.select wire:model.live="application.build_pack" label="Build Pack" required> <x-forms.select x-bind:disabled="initLoadingCompose" wire:model.live="application.build_pack"
label="Build Pack" required>
<option value="nixpacks">Nixpacks</option> <option value="nixpacks">Nixpacks</option>
<option value="static">Static</option> <option value="static">Static</option>
<option value="dockerfile">Dockerfile</option> <option value="dockerfile">Dockerfile</option>
@ -152,23 +145,24 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
@endif @endif
@endif @endif
@if ($application->build_pack === 'dockercompose') @if ($application->build_pack === 'dockercompose')
<div class="flex flex-col gap-2" wire:init='loadComposeFile(true)'> <div class="flex flex-col gap-2" x-init="$wire.dispatch('loadCompose', true)">
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.input placeholder="/" id="application.base_directory" label="Base Directory" <x-forms.input x-bind:disabled="initLoadingCompose" placeholder="/"
id="application.base_directory" label="Base Directory"
helper="Directory to use as root. Useful for monorepos." /> helper="Directory to use as root. Useful for monorepos." />
<x-forms.input placeholder="/docker-compose.yaml" id="application.docker_compose_location" <x-forms.input x-bind:disabled="initLoadingCompose" placeholder="/docker-compose.yaml"
label="Docker Compose Location" id="application.docker_compose_location" label="Docker Compose Location"
helper="It is calculated together with the Base Directory:<br><span class='dark:text-warning'>{{ Str::start($application->base_directory . $application->docker_compose_location, '/') }}</span>" /> helper="It is calculated together with the Base Directory:<br><span class='dark:text-warning'>{{ Str::start($application->base_directory . $application->docker_compose_location, '/') }}</span>" />
</div> </div>
<div class="pt-4">The following commands are for advanced use cases. Only modify them if you <div class="pt-4">The following commands are for advanced use cases. Only modify them if you
know what are know what are
you doing.</div> you doing.</div>
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.input placeholder="docker compose build" <x-forms.input placeholder="docker compose build" x-bind:disabled="initLoadingCompose"
id="application.docker_compose_custom_build_command" id="application.docker_compose_custom_build_command"
helper="If you use this, you need to specify paths relatively and should use the same compose file in the custom command, otherwise the automatically configured labels / etc won't work.<br><br>So in your case, use: <span class='dark:text-warning'>docker compose -f .{{ Str::start($application->base_directory . $application->docker_compose_location, '/') }} build</span>" helper="If you use this, you need to specify paths relatively and should use the same compose file in the custom command, otherwise the automatically configured labels / etc won't work.<br><br>So in your case, use: <span class='dark:text-warning'>docker compose -f .{{ Str::start($application->base_directory . $application->docker_compose_location, '/') }} build</span>"
label="Custom Build Command" /> label="Custom Build Command" />
<x-forms.input placeholder="docker compose up -d" <x-forms.input placeholder="docker compose up -d" x-bind:disabled="initLoadingCompose"
id="application.docker_compose_custom_start_command" id="application.docker_compose_custom_start_command"
helper="If you use this, you need to specify paths relatively and should use the same compose file in the custom command, otherwise the automatically configured labels / etc won't work.<br><br>So in your case, use: <span class='dark:text-warning'>docker compose -f .{{ Str::start($application->base_directory . $application->docker_compose_location, '/') }} up -d</span>" helper="If you use this, you need to specify paths relatively and should use the same compose file in the custom command, otherwise the automatically configured labels / etc won't work.<br><br>So in your case, use: <span class='dark:text-warning'>docker compose -f .{{ Str::start($application->base_directory . $application->docker_compose_location, '/') }} up -d</span>"
label="Custom Start Command" /> label="Custom Start Command" />
@ -220,7 +214,8 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
id="application.custom_docker_run_options" label="Custom Docker Options" /> id="application.custom_docker_run_options" label="Custom Docker Options" />
@endif @endif
@if ($application->build_pack === 'dockercompose') @if ($application->build_pack === 'dockercompose')
<x-forms.button wire:click="loadComposeFile">Reload Compose File</x-forms.button> <x-forms.button wire:target='initLoadingCompose'
x-on:click="$wire.dispatch('loadCompose', false)">Reload Compose File</x-forms.button>
@if ($application->settings->is_raw_compose_deployment_enabled) @if ($application->settings->is_raw_compose_deployment_enabled)
<x-forms.textarea rows="10" readonly id="application.docker_compose_raw" <x-forms.textarea rows="10" readonly id="application.docker_compose_raw"
label="Docker Compose Content (applicationId: {{ $application->id }})" label="Docker Compose Content (applicationId: {{ $application->id }})"
@ -257,18 +252,29 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
<h3 class="pt-8">Pre/Post Deployment Commands</h3> <h3 class="pt-8">Pre/Post Deployment Commands</h3>
<div class="flex flex-col gap-2 xl:flex-row"> <div class="flex flex-col gap-2 xl:flex-row">
<x-forms.input id="application.pre_deployment_command" label="Pre-deployment Command" <x-forms.input x-bind:disabled="initLoadingCompose" id="application.pre_deployment_command"
label="Pre-deployment Command"
helper="An optional script or command to execute in the existing container before the deployment begins." /> helper="An optional script or command to execute in the existing container before the deployment begins." />
<x-forms.input id="application.pre_deployment_command_container" label="Container Name" <x-forms.input x-bind:disabled="initLoadingCompose" id="application.pre_deployment_command_container"
label="Container Name"
helper="The name of the container to execute within. You can leave it blank if your application only has one container." /> helper="The name of the container to execute within. You can leave it blank if your application only has one container." />
</div> </div>
<div class="flex flex-col gap-2 xl:flex-row"> <div class="flex flex-col gap-2 xl:flex-row">
<x-forms.input placeholder="php artisan migrate" id="application.post_deployment_command" <x-forms.input x-bind:disabled="initLoadingCompose" placeholder="php artisan migrate"
label="Post-deployment Command" id="application.post_deployment_command" label="Post-deployment Command"
helper="An optional script or command to execute in the newly built container after the deployment completes." /> helper="An optional script or command to execute in the newly built container after the deployment completes." />
<x-forms.input id="application.post_deployment_command_container" label="Container Name" <x-forms.input x-bind:disabled="initLoadingCompose" id="application.post_deployment_command_container"
label="Container Name"
helper="The name of the container to execute within. You can leave it blank if your application only has one container." /> helper="The name of the container to execute within. You can leave it blank if your application only has one container." />
</div> </div>
</div> </div>
</form> </form>
@script
<script>
$wire.$on('loadCompose', (isInit = true) => {
$wire.initLoadingCompose = true;
$wire.loadComposeFile(isInit);
});
</script>
@endscript
</div> </div>

View File

@ -24,7 +24,7 @@ class="relative flex flex-col p-4 bg-white box-without-bg dark:bg-coolgray-100"
<div class="flex-1"></div> <div class="flex-1"></div>
@if (data_get($execution, 'status') === 'success') @if (data_get($execution, 'status') === 'success')
<x-forms.button class=" dark:hover:bg-coolgray-400" <x-forms.button class=" dark:hover:bg-coolgray-400"
wire:click="download({{ data_get($execution, 'id') }})">Download</x-forms.button> x-on:click="download_file('{{ data_get($execution, 'id') }}')">Download</x-forms.button>
@endif @endif
<x-modal-confirmation isErrorButton action="deleteBackup({{ data_get($execution, 'id') }})"> <x-modal-confirmation isErrorButton action="deleteBackup({{ data_get($execution, 'id') }})">
<x-slot:button-title> <x-slot:button-title>
@ -34,7 +34,13 @@ class="relative flex flex-col p-4 bg-white box-without-bg dark:bg-coolgray-100"
</x-modal-confirmation> </x-modal-confirmation>
</div> </div>
</form> </form>
@empty @empty
<div>No executions found.</div> <div>No executions found.</div>
@endforelse @endforelse
<script>
function download_file(executionId) {
window.open('/download/backup/' + executionId, '_blank');
}
</script>
</div> </div>

View File

@ -1,5 +1,6 @@
<div> <div>
<h1>Backups</h1> <h1>Backups</h1>
<livewire:project.shared.configuration-checker :resource="$database" />
<livewire:project.database.heading :database="$database" /> <livewire:project.database.heading :database="$database" />
<div class="pt-6"> <div class="pt-6">
<livewire:project.database.backup-edit :backup="$backup" :s3s="$s3s" :status="data_get($database, 'status')" /> <livewire:project.database.backup-edit :backup="$backup" :s3s="$s3s" :status="data_get($database, 'status')" />

View File

@ -1,5 +1,6 @@
<div> <div>
<h1>Backups</h1> <h1>Backups</h1>
<livewire:project.shared.configuration-checker :resource="$database" />
<livewire:project.database.heading :database="$database" /> <livewire:project.database.heading :database="$database" />
<div class="pt-6"> <div class="pt-6">
<div class="flex gap-2 "> <div class="flex gap-2 ">

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