diff --git a/README.md b/README.md index db38eb142..01c1ccef9 100644 --- a/README.md +++ b/README.md @@ -33,12 +33,13 @@ # Donations cccareers logo ## Github Sponsors ($40+) +BC Direct +typebot American Cloud CryptoJobsList -typebot -BC Direct UXWizz -Corentin Clichy +Younes Barrad +Automaze Corentin Clichy Niklas Lausch Pixel Infinito diff --git a/app/Actions/Database/StartClickhouse.php b/app/Actions/Database/StartClickhouse.php new file mode 100644 index 000000000..fb38c32d5 --- /dev/null +++ b/app/Actions/Database/StartClickhouse.php @@ -0,0 +1,159 @@ +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(); + } +} 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(); + + $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(); + } +} diff --git a/app/Actions/Database/StartKeydb.php b/app/Actions/Database/StartKeydb.php new file mode 100644 index 000000000..7b4bbe124 --- /dev/null +++ b/app/Actions/Database/StartKeydb.php @@ -0,0 +1,174 @@ +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}"); + } +} diff --git a/app/Actions/Database/StartMariadb.php b/app/Actions/Database/StartMariadb.php index 8dca584f2..755c47867 100644 --- a/app/Actions/Database/StartMariadb.php +++ b/app/Actions/Database/StartMariadb.php @@ -114,8 +114,12 @@ 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; + 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; } diff --git a/app/Actions/Database/StartMongodb.php b/app/Actions/Database/StartMongodb.php index ef04a9be8..180ae988a 100644 --- a/app/Actions/Database/StartMongodb.php +++ b/app/Actions/Database/StartMongodb.php @@ -130,8 +130,12 @@ 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; + 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; } diff --git a/app/Actions/Database/StartMysql.php b/app/Actions/Database/StartMysql.php index 3b47483a3..0e2e75719 100644 --- a/app/Actions/Database/StartMysql.php +++ b/app/Actions/Database/StartMysql.php @@ -114,8 +114,12 @@ 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; + 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; } diff --git a/app/Actions/Database/StartPostgresql.php b/app/Actions/Database/StartPostgresql.php index 844f81c31..03b460edc 100644 --- a/app/Actions/Database/StartPostgresql.php +++ b/app/Actions/Database/StartPostgresql.php @@ -136,8 +136,12 @@ 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; + 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; } diff --git a/app/Actions/Database/StartRedis.php b/app/Actions/Database/StartRedis.php index e1a8eef41..ff8c63aab 100644 --- a/app/Actions/Database/StartRedis.php +++ b/app/Actions/Database/StartRedis.php @@ -125,8 +125,12 @@ 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; + 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; } 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/Console/Kernel.php b/app/Console/Kernel.php index 835c0afe2..92cbf9c5c 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -33,6 +33,7 @@ protected function schedule(Schedule $schedule): void $this->check_scheduled_backups($schedule); $this->pull_helper_image($schedule); $this->check_scheduled_tasks($schedule); + $schedule->command('uploads:clear')->everyTwoMinutes(); } else { // Instance Jobs $schedule->command('horizon:snapshot')->everyFiveMinutes(); @@ -49,6 +50,7 @@ protected function schedule(Schedule $schedule): void $this->check_scheduled_tasks($schedule); $schedule->command('cleanup:database --yes')->daily(); + $schedule->command('uploads:clear')->everyTwoMinutes(); } } private function pull_helper_image($schedule) 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/Http/Controllers/UploadController.php b/app/Http/Controllers/UploadController.php new file mode 100644 index 000000000..e0a7d1b23 --- /dev/null +++ b/app/Http/Controllers/UploadController.php @@ -0,0 +1,83 @@ +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; + } +} diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 6ad87e07f..5cc5c4fb5 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -83,6 +83,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted private $env_nixpacks_args; private $docker_compose; private $docker_compose_base64; + private ?string $env_filename = null; private ?string $nixpacks_plan = null; private ?string $nixpacks_type = null; private string $dockerfile_location = '/Dockerfile'; @@ -239,50 +240,7 @@ public function handle(): void $this->build_server = $this->server; $this->original_server = $this->server; } - if ($this->restart_only && $this->application->build_pack !== 'dockerimage' && $this->application->build_pack !== 'dockerfile') { - $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); + $this->decide_what_to_do(); } catch (Exception $e) { 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); @@ -317,6 +275,43 @@ public function handle(): void 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() { if ($this->use_build_server) { @@ -336,7 +331,6 @@ private function deploy_simple_dockerfile() // if (!$this->force_rebuild) { // $this->check_image_locally_or_remotely(); // 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->generate_compose_file(); // $this->push_to_docker_registry(); @@ -477,7 +471,6 @@ private function deploy_dockerfile_buildpack() if (!$this->force_rebuild) { $this->check_image_locally_or_remotely(); 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->generate_compose_file(); $this->push_to_docker_registry(); @@ -506,7 +499,6 @@ private function deploy_nixpacks_buildpack() if (!$this->force_rebuild) { $this->check_image_locally_or_remotely(); 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->generate_compose_file(); ray('pushing to docker registry'); @@ -540,7 +532,6 @@ private function deploy_static_buildpack() if (!$this->force_rebuild) { $this->check_image_locally_or_remotely(); 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->generate_compose_file(); $this->push_to_docker_registry(); @@ -683,7 +674,7 @@ private function generate_image_names() } 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->check_git_if_build_needed(); $this->set_base_dir(); @@ -691,12 +682,14 @@ private function just_restart() $this->check_image_locally_or_remotely(); 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->create_workdir(); $this->generate_compose_file(); $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() { @@ -716,19 +709,35 @@ private function save_environment_variables() { $envs = collect([]); if ($this->pull_request_id !== 0) { + $this->env_filename = ".env-pr-$this->pull_request_id"; foreach ($this->application->environment_variables_preview as $env) { $envs->push($env->key . '=' . $env->real_value); } } else { + $this->env_filename = ".env"; foreach ($this->application->environment_variables as $env) { $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")); $this->execute_remote_command( [ 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" => "mkdir -p {$this->configuration_dir}" + ], ); } 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->execute_remote_command( + [ + "command" => "docker rm -f {$this->deployment_uuid}", + "ignore_errors" => true, + "hidden" => true + ], [ $runCommand, "hidden" => true, @@ -993,6 +1010,7 @@ private function clone_repository() $importCommands, "hidden" => true ] ); + $this->create_workdir(); } private function generate_git_import_commands() @@ -1098,6 +1116,7 @@ private function generate_env_variables() private function generate_compose_file() { + $this->create_workdir(); $ports = $this->application->settings->is_static ? [80] : $this->application->ports_exposes_array; $onlyPort = null; 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) { $docker_compose['services'][$this->container_name]['healthcheck'] = [ 'test' => [ @@ -1334,13 +1358,18 @@ private function generate_compose_file() $this->docker_compose = Yaml::dump($docker_compose, 10); $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->save_environment_variables(); } private function generate_local_persistent_volumes() { $local_persistent_volumes = []; 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) { $volume_name = $volume_name . '-pr-' . $this->pull_request_id; } diff --git a/app/Jobs/DeleteResourceJob.php b/app/Jobs/DeleteResourceJob.php index b84f66dfa..f2a611863 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, public bool $deleteConfigurations = false) { } @@ -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': @@ -49,6 +55,9 @@ public function handle() DeleteService::run($this->resource); break; } + if ($this->deleteConfigurations) { + $this->resource?->delete_configurations(); + } } catch (\Throwable $e) { ray($e->getMessage()); send_internal_notification('ContainerStoppingJob failed with: ' . $e->getMessage()); diff --git a/app/Livewire/ForcePasswordReset.php b/app/Livewire/ForcePasswordReset.php index 7bbec9d32..e4a66ebd6 100644 --- a/app/Livewire/ForcePasswordReset.php +++ b/app/Livewire/ForcePasswordReset.php @@ -24,7 +24,7 @@ public function mount() } public function render() { - return view('livewire.force-password-reset')->layout('layouts.simple'); + return view('livewire.force-password-reset'); } public function submit() { diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index a8032f6f8..7eb910533 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -24,7 +24,7 @@ class General extends Component public $customLabels; public bool $labelsChanged = false; - public bool $isConfigurationChanged = false; + public bool $initLoadingCompose = false; public ?string $initialDockerComposeLocation = null; public ?string $initialDockerComposePrLocation = null; @@ -123,19 +123,22 @@ public function mount() $this->application->settings->save(); } $this->parsedServiceDomains = $this->application->docker_compose_domains ? json_decode($this->application->docker_compose_domains, true) : []; - $this->ports_exposes = $this->application->ports_exposes; - if (str($this->application->status)->startsWith('running') && is_null($this->application->config_hash)) { - $this->application->isConfigurationChanged(true); - } - $this->isConfigurationChanged = $this->application->isConfigurationChanged(); - $this->customLabels = $this->application->parseContainerLabels(); + $this->customLabels = $this->application->parseContainerLabels(); if (!$this->customLabels && $this->application->destination->server->proxyType() !== 'NONE') { $this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n"); $this->application->custom_labels = base64_encode($this->customLabels); $this->application->save(); } $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() { @@ -154,11 +157,15 @@ public function loadComposeFile($isInit = false) } ['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation, 'initialDockerComposePrLocation' => $this->initialDockerComposePrLocation] = $this->application->loadComposeFile($isInit); $this->dispatch('success', 'Docker compose file loaded.'); + $this->dispatch('compose_loaded'); } catch (\Throwable $e) { $this->application->docker_compose_location = $this->initialDockerComposeLocation; $this->application->docker_compose_pr_location = $this->initialDockerComposePrLocation; $this->application->save(); return handleError($e, $this); + } finally { + $this->initLoadingCompose = false; + } } public function generateDomain(string $serviceName) @@ -307,7 +314,7 @@ public function submit($showToaster = true) } catch (\Throwable $e) { return handleError($e, $this); } finally { - $this->isConfigurationChanged = $this->application->isConfigurationChanged(); + $this->dispatch('configurationChanged'); } } } diff --git a/app/Livewire/Project/Application/Heading.php b/app/Livewire/Project/Application/Heading.php index 089eb5e9f..0717a51f0 100644 --- a/app/Livewire/Project/Application/Heading.php +++ b/app/Livewire/Project/Application/Heading.php @@ -22,6 +22,7 @@ public function getListeners() $teamId = auth()->user()->currentTeam()->id; return [ "echo-private:team.{$teamId},ApplicationStatusChanged" => 'check_status', + "compose_loaded" => '$refresh', ]; } public function mount() @@ -38,6 +39,7 @@ public function check_status($showNotification = false) } if ($showNotification) $this->dispatch('success', "Success", "Application status updated."); + $this->dispatch('configurationChanged'); } public function force_deploy_without_cache() 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..7fe9c1ce0 --- /dev/null +++ b/app/Livewire/Project/Database/Clickhouse/General.php @@ -0,0 +1,115 @@ + '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'); + } + } + } +} diff --git a/app/Livewire/Project/Database/Configuration.php b/app/Livewire/Project/Database/Configuration.php index c988477de..4ab8aa530 100644 --- a/app/Livewire/Project/Database/Configuration.php +++ b/app/Livewire/Project/Database/Configuration.php @@ -7,7 +7,8 @@ class Configuration extends Component { public $database; - 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,6 +22,10 @@ public function mount() { return redirect()->route('dashboard'); } $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() { diff --git a/app/Livewire/Project/Database/Dragonfly/General.php b/app/Livewire/Project/Database/Dragonfly/General.php new file mode 100644 index 000000000..0a4adf269 --- /dev/null +++ b/app/Livewire/Project/Database/Dragonfly/General.php @@ -0,0 +1,113 @@ + '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'); + } +} diff --git a/app/Livewire/Project/Database/Heading.php b/app/Livewire/Project/Database/Heading.php index 303166227..960ff2689 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; @@ -32,6 +34,12 @@ public function activityFinished() ]); $this->dispatch('refresh'); $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) @@ -71,6 +79,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..74e41056a 100644 --- a/app/Livewire/Project/Database/Import.php +++ b/app/Livewire/Project/Database/Import.php @@ -2,29 +2,25 @@ 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 { - use WithFileUploads; - - public $file; + public bool $unsupported = false; public $resource; public $parameters; public $containers; - public bool $validated = true; public bool $scpInProgress = 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 string $container; public array $importCommands = []; @@ -51,22 +47,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; @@ -75,38 +58,34 @@ public function getContainers() $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 ( - $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.'; + $this->unsupported = true; } } 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 { - $uploadedFilename = $this->file->store('backup-import'); + $uploadedFilename = "upload/{$this->resource->uuid}/restore"; $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); - - // SCP the backup file to the server. instant_scp($path, $tmpPath, $this->server); - $this->scpInProgress = false; - + Storage::delete($uploadedFilename); $this->importCommands[] = "docker cp {$tmpPath} {$this->container}:{$tmpPath}"; switch ($this->resource->getMorphClass()) { @@ -132,8 +111,7 @@ public function runImport() $this->dispatch('activityMonitor', $activity->id); } } catch (\Throwable $e) { - $this->validated = false; - $this->validationMsg = $e->getMessage(); + return handleError($e, $this); } } } diff --git a/app/Livewire/Project/Database/Keydb/General.php b/app/Livewire/Project/Database/Keydb/General.php new file mode 100644 index 000000000..536f743f2 --- /dev/null +++ b/app/Livewire/Project/Database/Keydb/General.php @@ -0,0 +1,117 @@ + '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'); + } +} diff --git a/app/Livewire/Project/Database/Mariadb/General.php b/app/Livewire/Project/Database/Mariadb/General.php index a4b6e1883..c0c67898f 100644 --- a/app/Livewire/Project/Database/Mariadb/General.php +++ b/app/Livewire/Project/Database/Mariadb/General.php @@ -76,6 +76,12 @@ public function submit() $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() diff --git a/app/Livewire/Project/Database/Mongodb/General.php b/app/Livewire/Project/Database/Mongodb/General.php index c1b55b1e7..3c1271065 100644 --- a/app/Livewire/Project/Database/Mongodb/General.php +++ b/app/Livewire/Project/Database/Mongodb/General.php @@ -78,6 +78,12 @@ public function submit() $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() diff --git a/app/Livewire/Project/Database/Mysql/General.php b/app/Livewire/Project/Database/Mysql/General.php index cab11e8ce..a1fb9201a 100644 --- a/app/Livewire/Project/Database/Mysql/General.php +++ b/app/Livewire/Project/Database/Mysql/General.php @@ -77,6 +77,12 @@ public function submit() $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() diff --git a/app/Livewire/Project/Database/Postgresql/General.php b/app/Livewire/Project/Database/Postgresql/General.php index 3237c1be3..79d91e7aa 100644 --- a/app/Livewire/Project/Database/Postgresql/General.php +++ b/app/Livewire/Project/Database/Postgresql/General.php @@ -58,7 +58,8 @@ public function mount() $this->db_url_public = $this->database->get_db_url(); } } - public function instantSaveAdvanced() { + public function instantSaveAdvanced() + { try { if (!$this->database->destination->server->isLogDrainEnabled()) { $this->database->is_log_drain_enabled = false; @@ -164,6 +165,12 @@ public function submit() $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'); + } } } } 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/Configuration.php b/app/Livewire/Project/Service/Configuration.php index cdedb3f8e..2cbda4e02 100644 --- a/app/Livewire/Project/Service/Configuration.php +++ b/app/Livewire/Project/Service/Configuration.php @@ -18,7 +18,8 @@ public function getListeners() $userId = auth()->user()->id; return [ "echo-private:user.{$userId},ServiceStatusChanged" => 'check_status', - "check_status" + "check_status", + "refresh" => '$refresh', ]; } public function render() @@ -65,7 +66,7 @@ public function check_status() try { dispatch_sync(new ContainerStatusJob($this->service->server)); $this->dispatch('refresh')->self(); - $this->dispatch('serviceStatusChanged'); + $this->dispatch('updateStatus'); } catch (\Exception $e) { return handleError($e, $this); } diff --git a/app/Livewire/Project/Service/EditCompose.php b/app/Livewire/Project/Service/EditCompose.php index cc385315e..84cc45cf2 100644 --- a/app/Livewire/Project/Service/EditCompose.php +++ b/app/Livewire/Project/Service/EditCompose.php @@ -18,7 +18,7 @@ public function mount() { } 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); } public function render() diff --git a/app/Livewire/Project/Service/EditDomain.php b/app/Livewire/Project/Service/EditDomain.php new file mode 100644 index 000000000..87a4a0546 --- /dev/null +++ b/app/Livewire/Project/Service/EditDomain.php @@ -0,0 +1,53 @@ + '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.

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'); + } +} 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/Service/Navbar.php b/app/Livewire/Project/Service/Navbar.php index 3462a96e1..141859ed4 100644 --- a/app/Livewire/Project/Service/Navbar.php +++ b/app/Livewire/Project/Service/Navbar.php @@ -17,21 +17,33 @@ class Navbar extends Component public array $query; 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() { $userId = auth()->user()->id; return [ "echo-private:user.{$userId},ServiceStatusChanged" => 'serviceStarted', - "serviceStatusChanged" + "updateStatus"=> '$refresh', ]; } - public function serviceStarted() { - $this->dispatch('success', 'Service status changed.'); - } - public function serviceStatusChanged() + public function serviceStarted() { - $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() { $this->dispatch('check_status'); diff --git a/app/Livewire/Project/Service/StackForm.php b/app/Livewire/Project/Service/StackForm.php index 1bfb70c5e..9c98c4d9f 100644 --- a/app/Livewire/Project/Service/StackForm.php +++ b/app/Livewire/Project/Service/StackForm.php @@ -69,6 +69,13 @@ public function submit() $this->dispatch('success', 'Service saved.'); } catch (\Throwable $e) { 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() diff --git a/app/Livewire/Project/Shared/ConfigurationChecker.php b/app/Livewire/Project/Shared/ConfigurationChecker.php new file mode 100644 index 000000000..930ac5fde --- /dev/null +++ b/app/Livewire/Project/Shared/ConfigurationChecker.php @@ -0,0 +1,34 @@ +configurationChanged(); + } + public function render() + { + return view('livewire.project.shared.configuration-checker'); + } + public function configurationChanged() + { + $this->isConfigurationChanged = $this->resource->isConfigurationChanged(); + } +} diff --git a/app/Livewire/Project/Shared/Danger.php b/app/Livewire/Project/Shared/Danger.php index f19cf8d53..2ed764cd1 100644 --- a/app/Livewire/Project/Shared/Danger.php +++ b/app/Livewire/Project/Shared/Danger.php @@ -11,6 +11,7 @@ class Danger extends Component public $resource; public $projectUuid; public $environmentName; + public bool $delete_configurations = true; public ?string $modalId = null; public function mount() @@ -20,12 +21,11 @@ public function mount() $this->projectUuid = data_get($parameters, 'project_uuid'); $this->environmentName = data_get($parameters, 'environment_name'); } - public function delete() { try { $this->resource->delete(); - DeleteResourceJob::dispatch($this->resource); + DeleteResourceJob::dispatch($this->resource, $this->delete_configurations); return redirect()->route('project.resource.index', [ 'project_uuid' => $this->projectUuid, 'environment_name' => $this->environmentName 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/Application.php b/app/Models/Application.php index 1b60914a7..204cb96b2 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -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() { return $this->belongsToMany(Server::class, 'additional_destinations') @@ -500,7 +509,7 @@ public function isLogDrainEnabled() } 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) { $newConfigHash .= json_encode($this->environment_variables()->get('updated_at')); } else { 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..03930fba4 100644 --- a/app/Models/EnvironmentVariable.php +++ b/app/Models/EnvironmentVariable.php @@ -32,7 +32,7 @@ protected static function booted() 'key' => $environment_variable->key, 'value' => $environment_variable->value, '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, 'is_preview' => true ]); @@ -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/Service.php b/app/Models/Service.php index b3430ac0a..9f0d2b67e 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -11,6 +11,46 @@ class Service extends BaseModel { use HasFactory, SoftDeletes; 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() { return 'service'; @@ -27,6 +67,14 @@ public function tags() { 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() { $applications = $this->applications; diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php new file mode 100644 index 000000000..a3a7cea8f --- /dev/null +++ b/app/Models/StandaloneClickhouse.php @@ -0,0 +1,210 @@ + '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'); + } +} 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..593b63506 --- /dev/null +++ b/app/Models/StandaloneDragonfly.php @@ -0,0 +1,210 @@ + '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'); + } +} diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php new file mode 100644 index 000000000..be9e66992 --- /dev/null +++ b/app/Models/StandaloneKeydb.php @@ -0,0 +1,212 @@ + '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'); + } + +} diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php index eb465e989..4c143d9b8 100644 --- a/app/Models/StandaloneMariadb.php +++ b/app/Models/StandaloneMariadb.php @@ -43,6 +43,45 @@ protected static function booted() $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() { return $this->getRawOriginal('status'); diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php index a8aab4e42..c23854376 100644 --- a/app/Models/StandaloneMongodb.php +++ b/app/Models/StandaloneMongodb.php @@ -46,6 +46,45 @@ protected static function booted() $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() { return $this->getRawOriginal('status'); diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php index 89155d3ec..7885b8441 100644 --- a/app/Models/StandaloneMysql.php +++ b/app/Models/StandaloneMysql.php @@ -43,6 +43,45 @@ protected static function booted() $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() { return $this->getRawOriginal('status'); diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php index 0b7589e81..b42dbdcb8 100644 --- a/app/Models/StandalonePostgresql.php +++ b/app/Models/StandalonePostgresql.php @@ -43,6 +43,45 @@ protected static function booted() $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() { return $this->getRawOriginal('status'); diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php index 2b623f28d..e6231be3c 100644 --- a/app/Models/StandaloneRedis.php +++ b/app/Models/StandaloneRedis.php @@ -38,6 +38,45 @@ protected static function booted() $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() { return $this->getRawOriginal('status'); 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/app/View/Components/Forms/Textarea.php b/app/View/Components/Forms/Textarea.php index 95e0ef5d2..86547cf65 100644 --- a/app/View/Components/Forms/Textarea.php +++ b/app/View/Components/Forms/Textarea.php @@ -22,6 +22,7 @@ public function __construct( public bool $required = false, public bool $disabled = false, public bool $readonly = false, + public bool $allowTab = false, public ?string $helper = null, public bool $realtimeValidation = false, public bool $allowToPeak = true, 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..7e12350fb 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_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. diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index aed77a7bb..afcc4622b 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -582,7 +582,7 @@ function validateComposeFile(string $compose, int $server_id): string|Throwable function escapeEnvVariables($value) { - $search = array("\\", "\r", "\t", "\x0", '"', "'", "$"); - $replace = array("\\\\", "\\r", "\\t", "\\0", '\"', "\'", "$$"); + $search = array("\\", "\r", "\t", "\x0", '"', "'"); + $replace = array("\\\\", "\\r", "\\t", "\\0", '\"', "\'"); return str_replace($search, $replace, $value); } 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/composer.json b/composer.json index b57263534..cb98eba57 100644 --- a/composer.json +++ b/composer.json @@ -27,6 +27,7 @@ "lorisleiva/laravel-actions": "^2.7", "nubs/random-name-generator": "^2.2", "phpseclib/phpseclib": "~3.0", + "pion/laravel-chunk-upload": "^1.5", "poliander/cron": "^3.0", "purplepixie/phpdns": "^2.1", "pusher/pusher-php-server": "^7.2", diff --git a/composer.lock b/composer.lock index 91613ec5f..55cd48f95 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e095b8a9eb22df2943cbc3e9649ff9e8", + "content-hash": "e6fd1d5c5183226a78df717b52343393", "packages": [ { "name": "amphp/amp", @@ -6370,6 +6370,72 @@ }, "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", "version": "3.1.0", diff --git a/config/chunk-upload.php b/config/chunk-upload.php new file mode 100644 index 000000000..a0baf8139 --- /dev/null +++ b/config/chunk-upload.php @@ -0,0 +1,44 @@ + [ + /* + * 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 + ], + ], +]; diff --git a/config/sentry.php b/config/sentry.php index 9e6f8ee9b..ac9f8c626 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -7,7 +7,7 @@ // The release version of your application // 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 'environment' => config('app.env'), diff --git a/config/version.php b/config/version.php index a307a90e6..f9a441c01 100644 --- a/config/version.php +++ b/config/version.php @@ -1,3 +1,3 @@ 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..55f070a74 --- /dev/null +++ b/database/migrations/2024_04_10_082220_create_standalone_dragonflies_table.php @@ -0,0 +1,63 @@ +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'); + }); + } +}; 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..e2732d443 --- /dev/null +++ b/database/migrations/2024_04_10_091519_create_standalone_clickhouses_table.php @@ -0,0 +1,64 @@ +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'); + }); + } +}; 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/database/migrations/2024_04_12_092337_add_config_hash_to_other_resources.php b/database/migrations/2024_04_12_092337_add_config_hash_to_other_resources.php new file mode 100644 index 000000000..83365f69e --- /dev/null +++ b/database/migrations/2024_04_12_092337_add_config_hash_to_other_resources.php @@ -0,0 +1,76 @@ +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'); + }); + } +}; diff --git a/docker/prod-ssu/etc/s6-overlay/s6-rc.d/horizon/run b/docker/prod-ssu/etc/s6-overlay/s6-rc.d/horizon/run index 012efc348..87471097e 100644 --- a/docker/prod-ssu/etc/s6-overlay/s6-rc.d/horizon/run +++ b/docker/prod-ssu/etc/s6-overlay/s6-rc.d/horizon/run @@ -1,2 +1,5 @@ #!/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" +} diff --git a/docker/prod-ssu/etc/s6-overlay/s6-rc.d/scheduler-worker/run b/docker/prod-ssu/etc/s6-overlay/s6-rc.d/scheduler-worker/run index 8021572af..87ca0cae1 100644 --- a/docker/prod-ssu/etc/s6-overlay/s6-rc.d/scheduler-worker/run +++ b/docker/prod-ssu/etc/s6-overlay/s6-rc.d/scheduler-worker/run @@ -1,2 +1,5 @@ #!/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" +} diff --git a/resources/css/app.css b/resources/css/app.css index 371efd8ed..719836599 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -289,3 +289,7 @@ .fullscreen { .toast { 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; +} diff --git a/resources/views/auth/reset-password.blade.php b/resources/views/auth/reset-password.blade.php index d227a7498..2e1a63d84 100644 --- a/resources/views/auth/reset-password.blade.php +++ b/resources/views/auth/reset-password.blade.php @@ -1,41 +1,41 @@ -
-
- -
+
+
+ + Coolify + +
{{ __('auth.reset_password') }}
-
-
- @csrf - - -
- - -
- {{ __('auth.reset_password') }} -
- @if ($errors->any()) -
- @foreach ($errors->all() as $error) -

{{ $error }}

- @endforeach -
- @endif - @if (session('status')) -
- {{ session('status') }} -
- @endif +
+
+
+ @csrf + + +
+ + +
+ {{ __('auth.reset_password') }} +
+ @if ($errors->any()) +
+ @foreach ($errors->all() as $error) +

{{ $error }}

+ @endforeach +
+ @endif + @if (session('status')) +
+ {{ session('status') }} +
+ @endif +
-
+
diff --git a/resources/views/components/forms/button.blade.php b/resources/views/components/forms/button.blade.php index 3eafc26c6..a109e84ac 100644 --- a/resources/views/components/forms/button.blade.php +++ b/resources/views/components/forms/button.blade.php @@ -5,11 +5,14 @@ @endisset @isset($confirmAction) x-on:{{ explode('(', $confirmAction)[0] }}.window="$wire.{{ explode('(', $confirmAction)[0] }}" - @endisset - > + @endisset> {{ $slot }} @if ($attributes->whereStartsWith('wire:click')->first()) - + + @elseif($attributes->whereStartsWith('wire:target')->first()) + @endif diff --git a/resources/views/components/forms/textarea.blade.php b/resources/views/components/forms/textarea.blade.php index e9bc4e8c1..a4b499c9c 100644 --- a/resources/views/components/forms/textarea.blade.php +++ b/resources/views/components/forms/textarea.blade.php @@ -1,3 +1,18 @@ + +
@if ($label)
@else -