diff --git a/app/Actions/Database/StartClickhouse.php b/app/Actions/Database/StartClickhouse.php new file mode 100644 index 000000000..d530bc6c3 --- /dev/null +++ b/app/Actions/Database/StartClickhouse.php @@ -0,0 +1,162 @@ +database = $database; + + $startCommand = "clickhouse-server"; + + $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' => [ + 'nofile' => [ + 'soft' => 262144, + 'hard' => 262144, + ], + ], + 'labels' => [ + 'coolify.managed' => 'true', + ], + 'user' => $this->database->clickhouse_user, + 'healthcheck' => [ + 'test' => "wget -qO- http://localhost:8123/ping || exit 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) { + $volume_name = $persistentStorage->host_path ?? $persistentStorage->name; + $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path; + } + return $local_persistent_volumes; + } + + private function generate_local_persistent_volumes_only_volume_names() + { + $local_persistent_volumes_names = []; + foreach ($this->database->persistentStorages as $persistentStorage) { + if ($persistentStorage->host_path) { + continue; + } + $name = $persistentStorage->name; + $local_persistent_volumes_names[$name] = [ + 'name' => $name, + 'external' => false, + ]; + } + return $local_persistent_volumes_names; + } + + private function generate_environment_variables() + { + $environment_variables = collect(); + foreach ($this->database->runtime_environment_variables as $env) { + $environment_variables->push("$env->key=$env->real_value"); + } + + if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('CLICKHOUSE_DB'))->isEmpty()) { + $environment_variables->push("CLICKHOUSE_DB={$this->database->clickhouse_db}"); + } + + if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('CLICKHOUSE_USER'))->isEmpty()) { + $environment_variables->push("CLICKHOUSE_USER={$this->database->clickhouse_user}"); + } + + if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('CLICKHOUSE_PASSWORD'))->isEmpty()) { + $environment_variables->push("CLICKHOUSE_PASSWORD={$this->database->clickhouse_password}"); + } + + return $environment_variables->all(); + } +} diff --git a/app/Actions/Database/StartDatabaseProxy.php b/app/Actions/Database/StartDatabaseProxy.php index 2a86ab913..89bee1d5a 100644 --- a/app/Actions/Database/StartDatabaseProxy.php +++ b/app/Actions/Database/StartDatabaseProxy.php @@ -3,6 +3,9 @@ namespace App\Actions\Database; use App\Models\ServiceDatabase; +use App\Models\StandaloneClickhouse; +use App\Models\StandaloneDragonfly; +use App\Models\StandaloneKeydb; use App\Models\StandaloneMariadb; use App\Models\StandaloneMongodb; use App\Models\StandaloneMysql; @@ -15,7 +18,7 @@ class StartDatabaseProxy { 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; $type = $database->getMorphClass(); @@ -50,6 +53,18 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St $type = 'App\Models\StandaloneRedis'; $containerName = "redis-{$database->service->uuid}"; 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') { @@ -62,6 +77,12 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St $internalPort = 3306; } else if ($type === 'App\Models\StandaloneMariadb') { $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); $nginxconf = <<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(); + $this->add_custom_dragonfly(); + + $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; + } + if (!is_null($this->database->dragonfly_conf)) { + $docker_compose['services'][$container_name]['volumes'][] = [ + 'type' => 'bind', + 'source' => $this->configuration_dir . '/dragonfly.conf', + 'target' => '/etc/dragonfly/dragonfly.conf', + 'read_only' => true, + ]; + $docker_compose['services'][$container_name]['command'] = "dragonfly /etc/dragonfly/dragonfly.conf --requirepass {$this->database->dragonfly_password}"; + } + $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) { + $volume_name = $persistentStorage->host_path ?? $persistentStorage->name; + $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path; + } + return $local_persistent_volumes; + } + + private function generate_local_persistent_volumes_only_volume_names() + { + $local_persistent_volumes_names = []; + foreach ($this->database->persistentStorages as $persistentStorage) { + if ($persistentStorage->host_path) { + continue; + } + $name = $persistentStorage->name; + $local_persistent_volumes_names[$name] = [ + 'name' => $name, + 'external' => false, + ]; + } + return $local_persistent_volumes_names; + } + + private function generate_environment_variables() + { + $environment_variables = collect(); + foreach ($this->database->runtime_environment_variables as $env) { + $environment_variables->push("$env->key=$env->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(); + } + private function add_custom_dragonfly() + { + if (is_null($this->database->dragonfly_conf)) { + return; + } + $filename = 'dragonfly.conf'; + Storage::disk('local')->put("tmp/dragonfly.conf_{$this->database->uuid}", $this->database->dragonfly_conf); + $path = Storage::path("tmp/dragonfly.conf_{$this->database->uuid}"); + instant_scp($path, "{$this->configuration_dir}/{$filename}", $this->database->destination->server); + Storage::disk('local')->delete("tmp/dragonfly.conf_{$this->database->uuid}"); + } +} diff --git a/app/Actions/Database/StartKeydb.php b/app/Actions/Database/StartKeydb.php new file mode 100644 index 000000000..11f8b0022 --- /dev/null +++ b/app/Actions/Database/StartKeydb.php @@ -0,0 +1,170 @@ +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) { + $volume_name = $persistentStorage->host_path ?? $persistentStorage->name; + $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path; + } + return $local_persistent_volumes; + } + + private function generate_local_persistent_volumes_only_volume_names() + { + $local_persistent_volumes_names = []; + foreach ($this->database->persistentStorages as $persistentStorage) { + if ($persistentStorage->host_path) { + continue; + } + $name = $persistentStorage->name; + $local_persistent_volumes_names[$name] = [ + 'name' => $name, + 'external' => false, + ]; + } + return $local_persistent_volumes_names; + } + + private function generate_environment_variables() + { + $environment_variables = collect(); + foreach ($this->database->runtime_environment_variables as $env) { + $environment_variables->push("$env->key=$env->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}"); + } +} diff --git a/app/Actions/Database/StopDatabase.php b/app/Actions/Database/StopDatabase.php index 019001070..408c5a69e 100644 --- a/app/Actions/Database/StopDatabase.php +++ b/app/Actions/Database/StopDatabase.php @@ -2,7 +2,9 @@ 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\StandaloneMongodb; use App\Models\StandaloneMysql; @@ -14,7 +16,7 @@ class StopDatabase { 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; if (!$server->isFunctional()) { diff --git a/app/Actions/Database/StopDatabaseProxy.php b/app/Actions/Database/StopDatabaseProxy.php index 6582da34e..984225435 100644 --- a/app/Actions/Database/StopDatabaseProxy.php +++ b/app/Actions/Database/StopDatabaseProxy.php @@ -3,6 +3,9 @@ namespace App\Actions\Database; use App\Models\ServiceDatabase; +use App\Models\StandaloneClickhouse; +use App\Models\StandaloneDragonfly; +use App\Models\StandaloneKeydb; use App\Models\StandaloneMariadb; use App\Models\StandaloneMongodb; use App\Models\StandaloneMysql; @@ -14,7 +17,7 @@ class StopDatabaseProxy { 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'); $uuid = $database->uuid; diff --git a/app/Console/Commands/CleanupStuckedResources.php b/app/Console/Commands/CleanupStuckedResources.php index d2882e9b3..32ba67a1e 100644 --- a/app/Console/Commands/CleanupStuckedResources.php +++ b/app/Console/Commands/CleanupStuckedResources.php @@ -7,6 +7,9 @@ use App\Models\Service; use App\Models\ServiceApplication; use App\Models\ServiceDatabase; +use App\Models\StandaloneClickhouse; +use App\Models\StandaloneDragonfly; +use App\Models\StandaloneKeydb; use App\Models\StandaloneMariadb; use App\Models\StandaloneMongodb; use App\Models\StandaloneMysql; @@ -55,6 +58,33 @@ private function cleanup_stucked_resources() } catch (\Throwable $e) { 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 { $mongodbs = StandaloneMongodb::withTrashed()->whereNotNull('deleted_at')->get(); foreach ($mongodbs as $mongodb) { diff --git a/app/Http/Controllers/Api/Deploy.php b/app/Http/Controllers/Api/Deploy.php index 5469ba1c6..f5798c52b 100644 --- a/app/Http/Controllers/Api/Deploy.php +++ b/app/Http/Controllers/Api/Deploy.php @@ -2,6 +2,9 @@ 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\StartMongodb; use App\Actions\Database\StartMysql; @@ -157,6 +160,24 @@ public function deploy_resource($resource, bool $force = false): array 'started_at' => now(), ]); $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') { StartMongodb::run($resource); $resource->update([ diff --git a/app/Jobs/DeleteResourceJob.php b/app/Jobs/DeleteResourceJob.php index b84f66dfa..22d5718ab 100644 --- a/app/Jobs/DeleteResourceJob.php +++ b/app/Jobs/DeleteResourceJob.php @@ -8,6 +8,9 @@ use App\Actions\Service\StopService; 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; @@ -25,7 +28,7 @@ class DeleteResourceJob implements ShouldQueue, ShouldBeEncrypted { 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) { } @@ -42,6 +45,9 @@ public function handle() case 'standalone-mongodb': case 'standalone-mysql': case 'standalone-mariadb': + case 'standalone-keydb': + case 'standalone-dragonfly': + case 'standalone-clickhouse': StopDatabase::run($this->resource); break; case 'service': diff --git a/app/Livewire/Project/Database/Backup/Index.php b/app/Livewire/Project/Database/Backup/Index.php index 6211a0e47..5a14c313b 100644 --- a/app/Livewire/Project/Database/Backup/Index.php +++ b/app/Livewire/Project/Database/Backup/Index.php @@ -8,7 +8,8 @@ class Index extends Component { public $database; public $s3s; - public function mount() { + public function mount() + { $project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first(); if (!$project) { return redirect()->route('dashboard'); @@ -21,8 +22,13 @@ public function mount() { if (!$database) { return redirect()->route('dashboard'); } - // No backups for redis - if ($database->getMorphClass() === 'App\Models\StandaloneRedis') { + // No backups + 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', [ 'project_uuid' => $project->uuid, 'environment_name' => $environment->name, diff --git a/app/Livewire/Project/Database/BackupExecutions.php b/app/Livewire/Project/Database/BackupExecutions.php index 5484dfdc8..b127a685c 100644 --- a/app/Livewire/Project/Database/BackupExecutions.php +++ b/app/Livewire/Project/Database/BackupExecutions.php @@ -4,6 +4,7 @@ use Illuminate\Support\Facades\Storage; use Livewire\Component; +use Symfony\Component\HttpFoundation\StreamedResponse; class BackupExecutions extends Component { @@ -36,32 +37,9 @@ public function deleteBackup($exeuctionId) $this->dispatch('success', 'Backup deleted.'); $this->refreshBackupExecutions(); } - public function download($exeuctionId) + public function download_file($exeuctionId) { - try { - $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); - } + return redirect()->route('download.backup', $exeuctionId); } public function refreshBackupExecutions(): void { diff --git a/app/Livewire/Project/Database/Clickhouse/General.php b/app/Livewire/Project/Database/Clickhouse/General.php new file mode 100644 index 000000000..29fb0cae2 --- /dev/null +++ b/app/Livewire/Project/Database/Clickhouse/General.php @@ -0,0 +1,131 @@ + 'required', + 'database.description' => 'nullable', + 'database.clickhouse_user' => 'required', + 'database.clickhouse_password' => 'required', + 'database.clickhouse_db' => '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_user' => 'Postgres User', + 'database.clickhouse_password' => 'Postgres Password', + 'database.clickhouse_db' => 'Postgres DB', + '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 save_init_script($script) + // { + // $this->database->init_scripts = filter($this->database->init_scripts, fn ($s) => $s['filename'] !== $script['filename']); + // $this->database->init_scripts = array_merge($this->database->init_scripts, [$script]); + // $this->database->save(); + // $this->dispatch('success', 'Init script saved.'); + // } + + // public function delete_init_script($script) + // { + // $collection = collect($this->database->init_scripts); + // $found = $collection->firstWhere('filename', $script['filename']); + // if ($found) { + // $this->database->init_scripts = $collection->filter(fn ($s) => $s['filename'] !== $script['filename'])->toArray(); + // $this->database->save(); + // $this->refresh(); + // $this->dispatch('success', 'Init script deleted.'); + // return; + // } + // } + + 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); + } + } +} diff --git a/app/Livewire/Project/Database/Dragonfly/General.php b/app/Livewire/Project/Database/Dragonfly/General.php new file mode 100644 index 000000000..6ba0a3a2d --- /dev/null +++ b/app/Livewire/Project/Database/Dragonfly/General.php @@ -0,0 +1,112 @@ + 'required', + 'database.description' => 'nullable', + 'database.dragonfly_conf' => '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_conf' => 'Redis Configuration', + '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(); + if ($this->database->dragonfly_conf === "") { + $this->database->dragonfly_conf = null; + } + $this->database->save(); + $this->dispatch('success', 'Database updated.'); + } 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 render() + { + return view('livewire.project.database.dragonfly.general'); + } +} diff --git a/app/Livewire/Project/Database/Heading.php b/app/Livewire/Project/Database/Heading.php index 303166227..406835871 100644 --- a/app/Livewire/Project/Database/Heading.php +++ b/app/Livewire/Project/Database/Heading.php @@ -2,13 +2,15 @@ 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\StartMongodb; use App\Actions\Database\StartMysql; use App\Actions\Database\StartPostgresql; use App\Actions\Database\StartRedis; use App\Actions\Database\StopDatabase; -use App\Events\DatabaseStatusChanged; use App\Jobs\ContainerStatusJob; use Livewire\Component; @@ -71,6 +73,15 @@ public function start() } else if ($this->database->type() === 'standalone-mariadb') { $activity = StartMariadb::run($this->database); $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); } } } diff --git a/app/Livewire/Project/Database/Import.php b/app/Livewire/Project/Database/Import.php index 42683c161..b27a567b0 100644 --- a/app/Livewire/Project/Database/Import.php +++ b/app/Livewire/Project/Database/Import.php @@ -2,15 +2,9 @@ namespace App\Livewire\Project\Database; -use Exception; use Livewire\Component; use Livewire\WithFileUploads; 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; class Import extends Component @@ -51,22 +45,9 @@ public function getContainers() if (!data_get($this->parameters, 'database_uuid')) { abort(404); } - - $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)) { - $resource = StandaloneRedis::where('uuid', $this->parameters['database_uuid'])->first(); - 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); - } - } - } - } + abort(404); } $this->resource = $resource; $this->server = $this->resource->destination->server; @@ -81,8 +62,11 @@ public function getContainers() } if ( - $this->resource->getMorphClass() == 'App\Models\StandaloneRedis' - || $this->resource->getMorphClass() == 'App\Models\StandaloneMongodb' + $this->resource->getMorphClass() == 'App\Models\StandaloneRedis' || + $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->validationMsg = 'This database type is not currently supported.'; diff --git a/app/Livewire/Project/Database/Keydb/General.php b/app/Livewire/Project/Database/Keydb/General.php new file mode 100644 index 000000000..bd99d8ddc --- /dev/null +++ b/app/Livewire/Project/Database/Keydb/General.php @@ -0,0 +1,111 @@ + '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); + } + } + 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'); + } +} diff --git a/app/Livewire/Project/New/Select.php b/app/Livewire/Project/New/Select.php index 2662cff09..d4deebeb4 100644 --- a/app/Livewire/Project/New/Select.php +++ b/app/Livewire/Project/New/Select.php @@ -120,6 +120,9 @@ public function setType(string $type) case 'mysql': case 'mariadb': case 'redis': + case 'keydb': + case 'dragonfly': + case 'clickhouse': case 'mongodb': $this->isDatabase = true; $this->includeSwarm = false; diff --git a/app/Livewire/Project/Resource/Create.php b/app/Livewire/Project/Resource/Create.php index 62ccac076..322360534 100644 --- a/app/Livewire/Project/Resource/Create.php +++ b/app/Livewire/Project/Resource/Create.php @@ -36,6 +36,12 @@ public function mount() $database = create_standalone_mysql($environment->id, $destination_uuid); } else if ($type->value() === 'mariadb') { $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', [ 'project_uuid' => $project->uuid, diff --git a/app/Livewire/Project/Resource/Index.php b/app/Livewire/Project/Resource/Index.php index 9524392dc..e3f3864c3 100644 --- a/app/Livewire/Project/Resource/Index.php +++ b/app/Livewire/Project/Resource/Index.php @@ -16,6 +16,9 @@ class Index extends Component public $mongodbs = []; public $mysqls = []; public $mariadbs = []; + public $keydbs = []; + public $dragonflies = []; + public $clickhouses = []; public $services = []; public function mount() { @@ -96,6 +99,39 @@ public function mount() } 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->services->map(function ($service) { if (data_get($service, 'environment.project.uuid')) { diff --git a/app/Livewire/Project/Service/FileStorage.php b/app/Livewire/Project/Service/FileStorage.php index 2abfcdf5e..0077f5cda 100644 --- a/app/Livewire/Project/Service/FileStorage.php +++ b/app/Livewire/Project/Service/FileStorage.php @@ -5,13 +5,14 @@ use App\Models\LocalFileVolume; use App\Models\ServiceApplication; use App\Models\ServiceDatabase; +use App\Models\StandaloneClickhouse; use Livewire\Component; use Illuminate\Support\Str; class FileStorage extends Component { public LocalFileVolume $fileStorage; - public ServiceApplication|ServiceDatabase $service; + public ServiceApplication|ServiceDatabase|StandaloneClickhouse $resource; public string $fs_path; public ?string $workdir = null; @@ -23,14 +24,14 @@ class FileStorage extends Component ]; public function mount() { - $this->service = $this->fileStorage->service; - if (Str::of($this->fileStorage->fs_path)->startsWith('.')) { - $this->workdir = $this->service->service->workdir(); + $this->resource = $this->fileStorage->service; + if (Str::of($this->fileStorage->fs_path)->startsWith('.')) { + $this->workdir = $this->resource->service->workdir(); $this->fs_path = Str::of($this->fileStorage->fs_path)->after('.'); - } else { + } else { $this->workdir = null; $this->fs_path = $this->fileStorage->fs_path; - } + } } public function submit() { diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php index c1a39afee..c4e14c905 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php @@ -119,6 +119,15 @@ public function saveVariables($isPreview) case 'standalone-mariadb': $environment->standalone_mariadb_id = $this->resource->id; 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': $environment->service_id = $this->resource->id; break; @@ -173,6 +182,15 @@ public function submit($data) case 'standalone-mariadb': $environment->standalone_mariadb_id = $this->resource->id; 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': $environment->service_id = $this->resource->id; break; diff --git a/app/Livewire/Project/Shared/ExecuteContainerCommand.php b/app/Livewire/Project/Shared/ExecuteContainerCommand.php index 7af7576bd..52d628dc1 100644 --- a/app/Livewire/Project/Shared/ExecuteContainerCommand.php +++ b/app/Livewire/Project/Shared/ExecuteContainerCommand.php @@ -5,11 +5,6 @@ use App\Models\Application; use App\Models\Server; 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 Livewire\Component; @@ -50,21 +45,9 @@ public function mount() } } else if (data_get($this->parameters, 'database_uuid')) { $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)) { - $resource = StandaloneRedis::where('uuid', $this->parameters['database_uuid'])->first(); - 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); - } - } - } - } + abort(404); } $this->resource = $resource; if ($this->resource->destination->server->isFunctional()) { @@ -109,7 +92,7 @@ public function loadContainers() ]; $this->containers = $this->containers->push($payload); } - } + } } if ($this->containers->count() > 0) { if (data_get($this->parameters, 'application_uuid')) { diff --git a/app/Livewire/Project/Shared/GetLogs.php b/app/Livewire/Project/Shared/GetLogs.php index 6d1aaefc1..996131f37 100644 --- a/app/Livewire/Project/Shared/GetLogs.php +++ b/app/Livewire/Project/Shared/GetLogs.php @@ -7,6 +7,9 @@ use App\Models\Service; use App\Models\ServiceApplication; use App\Models\ServiceDatabase; +use App\Models\StandaloneClickhouse; +use App\Models\StandaloneDragonfly; +use App\Models\StandaloneKeydb; use App\Models\StandaloneMariadb; use App\Models\StandaloneMongodb; use App\Models\StandaloneMysql; @@ -19,7 +22,7 @@ class GetLogs extends Component { public string $outputs = ''; 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 Server $server; public ?string $container = null; diff --git a/app/Livewire/Project/Shared/Logs.php b/app/Livewire/Project/Shared/Logs.php index d200ca69e..a2aaebd2b 100644 --- a/app/Livewire/Project/Shared/Logs.php +++ b/app/Livewire/Project/Shared/Logs.php @@ -5,6 +5,9 @@ use App\Models\Application; use App\Models\Server; 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; @@ -16,7 +19,7 @@ class Logs extends Component { 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 $containers; public $container = []; @@ -67,21 +70,9 @@ public function mount() } } else if (data_get($this->parameters, 'database_uuid')) { $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)) { - $resource = StandaloneRedis::where('uuid', $this->parameters['database_uuid'])->first(); - 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); - } - } - } - } + abort(404); } $this->resource = $resource; $this->status = $this->resource->status; diff --git a/app/Livewire/Project/Shared/ResourceOperations.php b/app/Livewire/Project/Shared/ResourceOperations.php index 62562179a..46f9021e5 100644 --- a/app/Livewire/Project/Shared/ResourceOperations.php +++ b/app/Livewire/Project/Shared/ResourceOperations.php @@ -76,7 +76,10 @@ public function cloneTo($destination_id) $this->resource->getMorphClass() === 'App\Models\StandaloneMongodb' || $this->resource->getMorphClass() === 'App\Models\StandaloneMysql' || $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); $new_resource = $this->resource->replicate()->fill([ diff --git a/app/Livewire/Project/Shared/Storages/All.php b/app/Livewire/Project/Shared/Storages/All.php index 598bc63d5..a17153343 100644 --- a/app/Livewire/Project/Shared/Storages/All.php +++ b/app/Livewire/Project/Shared/Storages/All.php @@ -2,7 +2,6 @@ namespace App\Livewire\Project\Shared\Storages; -use App\Models\LocalPersistentVolume; use Livewire\Component; class All extends Component diff --git a/app/Livewire/Server/Form.php b/app/Livewire/Server/Form.php index ff9aaf701..e51aff8a3 100644 --- a/app/Livewire/Server/Form.php +++ b/app/Livewire/Server/Form.php @@ -75,6 +75,7 @@ public function revalidate() } public function checkLocalhostConnection() { + $this->submit(); $uptime = $this->server->validateConnection(); if ($uptime) { $this->dispatch('success', 'Server is reachable.'); diff --git a/app/Models/Environment.php b/app/Models/Environment.php index efbfc70d9..7ed9e38e5 100644 --- a/app/Models/Environment.php +++ b/app/Models/Environment.php @@ -46,7 +46,18 @@ public function mariadbs() { 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() { $postgresqls = $this->postgresqls; @@ -54,7 +65,10 @@ public function databases() $mongodbs = $this->mongodbs; $mysqls = $this->mysqls; $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() diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php index 32277769e..3bf1404dd 100644 --- a/app/Models/EnvironmentVariable.php +++ b/app/Models/EnvironmentVariable.php @@ -63,19 +63,7 @@ public function resource() } else if ($this->service_id) { $resource = Service::find($this->service_id); } else if ($this->database_id) { - $resource = StandalonePostgresql::find($this->database_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); - } - } - } - } + $resource = getResourceByUuid($this->parameters['database_uuid'], data_get(auth()->user()->currentTeam(), 'id')); } return $resource; } diff --git a/app/Models/LocalFileVolume.php b/app/Models/LocalFileVolume.php index b097aa300..7ea0cd546 100644 --- a/app/Models/LocalFileVolume.php +++ b/app/Models/LocalFileVolume.php @@ -22,8 +22,14 @@ public function service() } public function saveStorageOnServer() { - $workdir = $this->resource->service->workdir(); - $server = $this->resource->service->server; + $isService = data_get($this->resource, 'service'); + if ($isService) { + $workdir = $this->resource->service->workdir(); + $server = $this->resource->service->server; + } else { + $workdir = $this->resource->workdir(); + $server = $this->resource->destination->server; + } $commands = collect([ "mkdir -p $workdir > /dev/null 2>&1 || true", "cd $workdir" @@ -55,8 +61,18 @@ public function saveStorageOnServer() if (!$fileVolume->is_directory && $isDir == 'NOK') { if ($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("chmod +x $path"); + if ($chown) { + $commands->push("chown $chown $path"); + } + if ($chmod) { + $commands->push("chmod $chmod $path"); + } + } } else if ($isDir == 'NOK' && $fileVolume->is_directory) { $commands->push("mkdir -p $path > /dev/null 2>&1 || true"); diff --git a/app/Models/Project.php b/app/Models/Project.php index 27ae10778..2621d3da1 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -63,6 +63,18 @@ public function redis() { 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() { return $this->hasManyThrough(StandaloneMongodb::class, Environment::class); @@ -77,6 +89,6 @@ public function mariadbs() } 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(); } } diff --git a/app/Models/Server.php b/app/Models/Server.php index bcb06954a..79c98ccf6 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -585,7 +585,10 @@ public function databases() $mongodbs = data_get($standaloneDocker, 'mongodbs', collect([])); $mysqls = data_get($standaloneDocker, 'mysqls', 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) { return data_get($item, 'name') !== 'coolify-db'; })->flatten(); diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php new file mode 100644 index 000000000..35eaf371a --- /dev/null +++ b/app/Models/StandaloneClickhouse.php @@ -0,0 +1,222 @@ + 'encrypted', + ]; + + protected static function booted() + { + static::created(function ($database) { + LocalPersistentVolume::create([ + 'name' => 'clickhouse-data-' . $database->uuid, + 'mount_path' => '/var/lib/clickhouse/', + 'host_path' => null, + 'resource_id' => $database->id, + 'resource_type' => $database->getMorphClass(), + 'is_readonly' => true + ]); + LocalPersistentVolume::create([ + 'name' => 'clickhouse-logs-' . $database->uuid, + 'mount_path' => '/var/log/clickhouse-server/', + 'host_path' => null, + 'resource_id' => $database->id, + 'resource_type' => $database->getMorphClass(), + 'is_readonly' => true + ]); +// LocalFileVolume::create( +// [ +// 'mount_path' => '/etc/clickhouse-server/config.d/docker_related_config.xml', +// 'resource_id' => $database->id, +// 'resource_type' => $database->getMorphClass(), +// 'chown' => '101:101', +// 'chmod' => '644', +// 'fs_path' => database_configuration_dir() . '/' . $database->uuid . '/config.d/docker_related_config.xml', +// 'content' => ' +// +// :: +// 0.0.0.0 +// 1 + +// +// ', +// 'is_directory' => 'false', +// ] +// ); + // LocalPersistentVolume::create([ + // 'name' => 'clickhouse-config-' . $database->uuid, + // 'mount_path' => '/etc/clickhouse-server/config.d', + // 'host_path' => database_configuration_dir() . '/' . $database->uuid . '/config.d', + // 'resource_id' => $database->id, + // 'resource_type' => $database->getMorphClass(), + // 'is_readonly' => true + // ]); + // LocalPersistentVolume::create([ + // 'name' => 'clickhouse-config-users-' . $database->uuid, + // 'mount_path' => '/etc/clickhouse-server/users.d', + // 'host_path' => database_configuration_dir() . '/' . $database->uuid . '/users.d', + // '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 workdir() + { + return database_configuration_dir() . '/' . $this->uuid; + } + 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'); + } +} diff --git a/app/Models/StandaloneDocker.php b/app/Models/StandaloneDocker.php index cba7122ca..228a82086 100644 --- a/app/Models/StandaloneDocker.php +++ b/app/Models/StandaloneDocker.php @@ -32,6 +32,18 @@ public function mariadbs() { 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() { diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php new file mode 100644 index 000000000..7709f1f07 --- /dev/null +++ b/app/Models/StandaloneDragonfly.php @@ -0,0 +1,172 @@ + '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 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'); + } +} diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php new file mode 100644 index 000000000..3194ba7ba --- /dev/null +++ b/app/Models/StandaloneKeydb.php @@ -0,0 +1,173 @@ + '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 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'); + } + +} diff --git a/app/Models/SwarmDocker.php b/app/Models/SwarmDocker.php index 9f0973db5..a14131f43 100644 --- a/app/Models/SwarmDocker.php +++ b/app/Models/SwarmDocker.php @@ -20,6 +20,18 @@ public function redis() { 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() { return $this->morphMany(StandaloneMongodb::class, 'destination'); @@ -50,7 +62,10 @@ public function databases() $mongodbs = $this->mongodbs; $mysqls = $this->mysqls; $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() diff --git a/bootstrap/helpers/constants.php b/bootstrap/helpers/constants.php index ee9a74b72..c3ef9a694 100644 --- a/bootstrap/helpers/constants.php +++ b/bootstrap/helpers/constants.php @@ -1,7 +1,7 @@ '; -const DATABASE_TYPES = ['postgresql', 'redis', 'mongodb', 'mysql', 'mariadb']; +const DATABASE_TYPES = ['postgresql', 'redis', 'mongodb', 'mysql', 'mariadb', 'keydb', 'dragonfly', 'clickhouse']; const VALID_CRON_STRINGS = [ 'every_minute' => '* * * * *', 'hourly' => '0 * * * *', diff --git a/bootstrap/helpers/databases.php b/bootstrap/helpers/databases.php index cef5ac7fd..9eb04ddb8 100644 --- a/bootstrap/helpers/databases.php +++ b/bootstrap/helpers/databases.php @@ -1,7 +1,10 @@ $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_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. diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index d40c707e4..4533d098b 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -11,6 +11,9 @@ use App\Models\Service; use App\Models\ServiceApplication; use App\Models\ServiceDatabase; +use App\Models\StandaloneClickhouse; +use App\Models\StandaloneDragonfly; +use App\Models\StandaloneKeydb; use App\Models\StandaloneMariadb; use App\Models\StandaloneMongodb; use App\Models\StandaloneMysql; @@ -463,15 +466,14 @@ function getServiceTemplates() function getResourceByUuid(string $uuid, ?int $teamId = null) { - $resource = queryResourcesByUuid($uuid); - if (!is_null($teamId)) { - if (!is_null($resource) && $resource->environment->project->team_id === $teamId) { - return $resource; - } + if (is_null($teamId)) { return null; - } else { + } + $resource = queryResourcesByUuid($uuid); + if (!is_null($resource) && $resource->environment->project->team_id === $teamId) { return $resource; } + return null; } function queryResourcesByUuid(string $uuid) { @@ -490,6 +492,12 @@ function queryResourcesByUuid(string $uuid) if ($mysql) return $mysql; $mariadb = StandaloneMariadb::whereUuid($uuid)->first(); 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; } function generatTagDeployWebhook($tag_name) diff --git a/database/migrations/2024_04_10_071920_create_standalone_keydbs_table.php b/database/migrations/2024_04_10_071920_create_standalone_keydbs_table.php new file mode 100644 index 000000000..e336db0d8 --- /dev/null +++ b/database/migrations/2024_04_10_071920_create_standalone_keydbs_table.php @@ -0,0 +1,64 @@ +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'); + }); + } +}; diff --git a/database/migrations/2024_04_10_082220_create_standalone_dragonflies_table.php b/database/migrations/2024_04_10_082220_create_standalone_dragonflies_table.php new file mode 100644 index 000000000..23b99b6d7 --- /dev/null +++ b/database/migrations/2024_04_10_082220_create_standalone_dragonflies_table.php @@ -0,0 +1,64 @@ +id(); + $table->string('uuid')->unique(); + $table->string('name'); + $table->string('description')->nullable(); + + $table->text('dragonfly_password'); + $table->longText('dragonfly_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('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'); + }); + } +}; diff --git a/database/migrations/2024_04_10_091519_create_standalone_clickhouses_table.php b/database/migrations/2024_04_10_091519_create_standalone_clickhouses_table.php new file mode 100644 index 000000000..f0b5dc12c --- /dev/null +++ b/database/migrations/2024_04_10_091519_create_standalone_clickhouses_table.php @@ -0,0 +1,65 @@ +id(); + $table->string('uuid')->unique(); + $table->string('name'); + $table->string('description')->nullable(); + + $table->string('clickhouse_user')->default('clickhouse'); + $table->text('clickhouse_password'); + $table->string('clickhouse_db')->default('default'); + + $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('clickhouse/clickhouse-server'); + + $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'); + }); + } +}; diff --git a/database/migrations/2024_04_10_124015_add_permission_local_file_volumes.php b/database/migrations/2024_04_10_124015_add_permission_local_file_volumes.php new file mode 100644 index 000000000..a2487ccd6 --- /dev/null +++ b/database/migrations/2024_04_10_124015_add_permission_local_file_volumes.php @@ -0,0 +1,30 @@ +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'); + }); + } +}; diff --git a/resources/views/livewire/project/database/clickhouse/general.blade.php b/resources/views/livewire/project/database/clickhouse/general.blade.php new file mode 100644 index 000000000..4f0a89c02 --- /dev/null +++ b/resources/views/livewire/project/database/clickhouse/general.blade.php @@ -0,0 +1,61 @@ +
+
+
+

General

+ + Save + +
+
+ + + +
+ + @if ($database->started_at) +
+ + + +
+ @else +
Please verify these values. You can only modify them before the initial + start. After that, you need to modify it in the database. +
+
+ + + +
+ @endif +
+

Network

+
+ + + +
+ + @if ($db_url_public) + + @endif +
+
+

Advanced

+
+ +
+
diff --git a/resources/views/livewire/project/database/configuration.blade.php b/resources/views/livewire/project/database/configuration.blade.php index 725fb1233..f1b9f6e9e 100644 --- a/resources/views/livewire/project/database/configuration.blade.php +++ b/resources/views/livewire/project/database/configuration.blade.php @@ -59,6 +59,12 @@ @elseif ($database->type() === 'standalone-mariadb') + @elseif ($database->type() === 'standalone-keydb') + + @elseif ($database->type() === 'standalone-dragonfly') + + @elseif ($database->type() === 'standalone-clickhouse') + @endif
diff --git a/resources/views/livewire/project/database/dragonfly/general.blade.php b/resources/views/livewire/project/database/dragonfly/general.blade.php new file mode 100644 index 000000000..befd000c2 --- /dev/null +++ b/resources/views/livewire/project/database/dragonfly/general.blade.php @@ -0,0 +1,41 @@ +
+
+
+

General

+ + Save + +
+
+ + + +
+
+

Network

+
+ + + +
+ + @if ($db_url_public) + + @endif +
+ {{-- --}} +

Advanced

+
+ +
+ +
diff --git a/resources/views/livewire/project/database/keydb/general.blade.php b/resources/views/livewire/project/database/keydb/general.blade.php new file mode 100644 index 000000000..69d3fa908 --- /dev/null +++ b/resources/views/livewire/project/database/keydb/general.blade.php @@ -0,0 +1,42 @@ +
+
+
+

General

+ + Save + +
+
+ + + +
+
+

Network

+
+ + + +
+ + @if ($db_url_public) + + @endif +
+ +

Advanced

+
+ +
+ +
diff --git a/resources/views/livewire/project/new/select.blade.php b/resources/views/livewire/project/new/select.blade.php index eea43a8bd..c01cd51a9 100644 --- a/resources/views/livewire/project/new/select.blade.php +++ b/resources/views/livewire/project/new/select.blade.php @@ -264,6 +264,125 @@ class="w-[4.5rem]
+ + DragonFly + + Dragonfly is a simple, performant, and cost-efficient in-memory data store, fully compatible + with Redis APIs but without the Redis management complexity. + + +
+ + + + + +
+
+
+ + KeyDB + + KeyDB is a fully open source database, backed by Snap, and a faster drop in alternative to + Redis. + + +
+ + + + + + + + + + + + + + + + + + +
+
+
+ + Clickhouse + + Clickhouse is an open-source column-oriented database management system. + + +
+ + + + + + + + + +
+
+
MongoDB diff --git a/resources/views/livewire/project/resource/index.blade.php b/resources/views/livewire/project/resource/index.blade.php index ed7641bb1..61ee13ceb 100644 --- a/resources/views/livewire/project/resource/index.blade.php +++ b/resources/views/livewire/project/resource/index.blade.php @@ -240,6 +240,102 @@ class="items-center justify-center box">+ Add New Resource + + +