diff --git a/README.md b/README.md index 031e39c2a..3d3e1abf5 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Coolify is an open-source & self-hostable alternative to Heroku / Netlify / Verc It helps you to manage your servers, applications, databases on your own hardware, all you need is SSH connection. You can manage VPS, Bare Metal, Raspberry PI's anything. -Image if you could have the ease of a cloud but with your own servers. That is **Coolify**. +Imagine if you could have the ease of a cloud but with your own servers. That is **Coolify**. No vendor lock-in, which means that all the configuration for your applications/databases/etc are saved to your server. So if you decide to stop using Coolify (oh nooo), you could still manage your running resources. You just lose the automations and all the magic. 🪄️ diff --git a/app/Actions/Database/StartDatabaseProxy.php b/app/Actions/Database/StartDatabaseProxy.php index eccdf1a6a..15009019d 100644 --- a/app/Actions/Database/StartDatabaseProxy.php +++ b/app/Actions/Database/StartDatabaseProxy.php @@ -2,6 +2,7 @@ namespace App\Actions\Database; +use App\Models\StandaloneMongodb; use App\Models\StandalonePostgresql; use App\Models\StandaloneRedis; use Lorisleiva\Actions\Concerns\AsAction; @@ -11,13 +12,15 @@ class StartDatabaseProxy { use AsAction; - public function handle(StandaloneRedis|StandalonePostgresql $database) + public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb $database) { $internalPort = null; - if ($database->getMorphClass()=== 'App\Models\StandaloneRedis') { + if ($database->getMorphClass() === 'App\Models\StandaloneRedis') { $internalPort = 6379; - } else if ($database->getMorphClass()=== 'App\Models\StandalonePostgresql') { + } else if ($database->getMorphClass() === 'App\Models\StandalonePostgresql') { $internalPort = 5432; + } else if ($database->getMorphClass() === 'App\Models\StandaloneMongodb') { + $internalPort = 27017; } $containerName = "{$database->uuid}-proxy"; $configuration_dir = database_proxy_dir($database->uuid); diff --git a/app/Actions/Database/StartMongodb.php b/app/Actions/Database/StartMongodb.php new file mode 100644 index 000000000..fbdf0bba4 --- /dev/null +++ b/app/Actions/Database/StartMongodb.php @@ -0,0 +1,163 @@ +database = $database; + + $startCommand = "mongod"; + + $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_mongo_conf(); + + $docker_compose = [ + 'version' => '3.8', + 'services' => [ + $container_name => [ + 'image' => $this->database->image, + 'command' => $startCommand, + 'container_name' => $container_name, + 'environment' => $environment_variables, + 'restart' => RESTART_MODE, + 'networks' => [ + $this->database->destination->network, + ], + 'labels' => [ + 'coolify.managed' => 'true', + ], + 'healthcheck' => [ + 'test' => [ + 'CMD-SHELL', + 'mongo --eval "printjson(db.serverStatus())" | grep uptime | grep -v grep' + ], + 'interval' => '5s', + 'timeout' => '5s', + 'retries' => 10, + 'start_period' => '5s' + ], + 'mem_limit' => $this->database->limits_memory, + 'memswap_limit' => $this->database->limits_memory_swap, + 'mem_swappiness' => $this->database->limits_memory_swappiness, + 'mem_reservation' => $this->database->limits_memory_reservation, + 'cpus' => $this->database->limits_cpus, + 'cpuset' => $this->database->limits_cpuset, + 'cpu_shares' => $this->database->limits_cpu_shares, + ] + ], + 'networks' => [ + $this->database->destination->network => [ + 'external' => true, + 'name' => $this->database->destination->network, + 'attachable' => true, + ] + ] + ]; + if (count($this->database->ports_mappings_array) > 0) { + $docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array; + } + if (count($persistent_storages) > 0) { + $docker_compose['services'][$container_name]['volumes'] = $persistent_storages; + } + if (count($volume_names) > 0) { + $docker_compose['volumes'] = $volume_names; + } + if (!is_null($this->database->mongo_conf)) { + $docker_compose['services'][$container_name]['volumes'][] = [ + 'type' => 'bind', + 'source' => $this->configuration_dir . '/mongod.conf', + 'target' => '/etc/mongo/mongod.conf', + 'read_only' => true, + ]; + $docker_compose['services'][$container_name]['command'] = $startCommand . ' --config /etc/mongo/mongod.conf'; + } + $docker_compose = Yaml::dump($docker_compose, 10); + $docker_compose_base64 = base64_encode($docker_compose); + $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml"; + $readme = generate_readme_file($this->database->name, now()); + $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; + $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; + $this->commands[] = "echo '####### {$database->name} started.'"; + return remote_process($this->commands, $database->destination->server); + } + + private function generate_local_persistent_volumes() + { + $local_persistent_volumes = []; + foreach ($this->database->persistentStorages as $persistentStorage) { + $volume_name = $persistentStorage->host_path ?? $persistentStorage->name; + $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path; + } + return $local_persistent_volumes; + } + + private function generate_local_persistent_volumes_only_volume_names() + { + $local_persistent_volumes_names = []; + foreach ($this->database->persistentStorages as $persistentStorage) { + if ($persistentStorage->host_path) { + continue; + } + $name = $persistentStorage->name; + $local_persistent_volumes_names[$name] = [ + 'name' => $name, + 'external' => false, + ]; + } + return $local_persistent_volumes_names; + } + + private function generate_environment_variables() + { + $environment_variables = collect(); + foreach ($this->database->runtime_environment_variables as $env) { + $environment_variables->push("$env->key=$env->value"); + } + + if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MONGO_INITDB_ROOT_USERNAME'))->isEmpty()) { + $environment_variables->push("MONGO_INITDB_ROOT_USERNAME={$this->database->mongo_initdb_root_username}"); + } + + if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MONGO_INITDB_ROOT_PASSWORD'))->isEmpty()) { + $environment_variables->push("MONGO_INITDB_ROOT_PASSWORD={$this->database->mongo_initdb_root_password}"); + } + + if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MONGO_INITDB_DATABASE'))->isEmpty()) { + $environment_variables->push("MONGO_INITDB_DATABASE={$this->database->mongo_initdb_database}"); + } + return $environment_variables->all(); + } + private function add_custom_mongo_conf() + { + if (is_null($this->database->mongo_conf)) { + return; + } + $filename = 'mongod.conf'; + $content = $this->database->mongo_conf; + $content_base64 = base64_encode($content); + $this->commands[] = "echo '{$content_base64}' | base64 -d > $this->configuration_dir/{$filename}"; + } +} diff --git a/app/Actions/Database/StartPostgresql.php b/app/Actions/Database/StartPostgresql.php index 50d8b8541..46e410980 100644 --- a/app/Actions/Database/StartPostgresql.php +++ b/app/Actions/Database/StartPostgresql.php @@ -2,7 +2,6 @@ namespace App\Actions\Database; -use App\Models\Server; use App\Models\StandalonePostgresql; use Illuminate\Support\Str; use Symfony\Component\Yaml\Yaml; @@ -17,7 +16,7 @@ class StartPostgresql public array $init_scripts = []; public string $configuration_dir; - public function handle(Server $server, StandalonePostgresql $database) + public function handle(StandalonePostgresql $database) { $this->database = $database; $container_name = $this->database->uuid; @@ -104,7 +103,7 @@ class StartPostgresql $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "echo '####### {$database->name} started.'"; - return remote_process($this->commands, $server); + return remote_process($this->commands, $database->destination->server); } private function generate_local_persistent_volumes() @@ -145,6 +144,9 @@ class StartPostgresql if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('POSTGRES_USER'))->isEmpty()) { $environment_variables->push("POSTGRES_USER={$this->database->postgres_user}"); } + if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('PGUSER'))->isEmpty()) { + $environment_variables->push("PGUSER={$this->database->postgres_user}"); + } if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('POSTGRES_PASSWORD'))->isEmpty()) { $environment_variables->push("POSTGRES_PASSWORD={$this->database->postgres_password}"); diff --git a/app/Actions/Database/StartRedis.php b/app/Actions/Database/StartRedis.php index 6ea950f5d..af6b2ad4f 100644 --- a/app/Actions/Database/StartRedis.php +++ b/app/Actions/Database/StartRedis.php @@ -2,7 +2,6 @@ namespace App\Actions\Database; -use App\Models\Server; use App\Models\StandaloneRedis; use Illuminate\Support\Str; use Symfony\Component\Yaml\Yaml; @@ -17,7 +16,7 @@ class StartRedis public string $configuration_dir; - public function handle(Server $server, StandaloneRedis $database) + public function handle(StandaloneRedis $database) { $this->database = $database; @@ -104,7 +103,7 @@ class StartRedis $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "echo '####### {$database->name} started.'"; - return remote_process($this->commands, $server); + return remote_process($this->commands, $database->destination->server); } private function generate_local_persistent_volumes() diff --git a/app/Actions/Database/StopDatabase.php b/app/Actions/Database/StopDatabase.php index ee1913f06..7e3f5f4c2 100644 --- a/app/Actions/Database/StopDatabase.php +++ b/app/Actions/Database/StopDatabase.php @@ -2,16 +2,16 @@ namespace App\Actions\Database; +use App\Models\StandaloneMongodb; use App\Models\StandalonePostgresql; use App\Models\StandaloneRedis; -use App\Notifications\Application\StatusChanged; use Lorisleiva\Actions\Concerns\AsAction; class StopDatabase { use AsAction; - public function handle(StandaloneRedis|StandalonePostgresql $database) + public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb $database) { $server = $database->destination->server; instant_remote_process( diff --git a/app/Actions/Database/StopDatabaseProxy.php b/app/Actions/Database/StopDatabaseProxy.php index ba836ec3a..840e8ed56 100644 --- a/app/Actions/Database/StopDatabaseProxy.php +++ b/app/Actions/Database/StopDatabaseProxy.php @@ -2,6 +2,7 @@ namespace App\Actions\Database; +use App\Models\StandaloneMongodb; use App\Models\StandalonePostgresql; use App\Models\StandaloneRedis; use Lorisleiva\Actions\Concerns\AsAction; @@ -10,7 +11,7 @@ class StopDatabaseProxy { use AsAction; - public function handle(StandaloneRedis|StandalonePostgresql $database) + public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb $database) { instant_remote_process(["docker rm -f {$database->uuid}-proxy"], $database->destination->server); $database->is_public = false; diff --git a/app/Console/Commands/GenerateServiceTemplates.php b/app/Console/Commands/GenerateServiceTemplates.php new file mode 100644 index 000000000..6220a3774 --- /dev/null +++ b/app/Console/Commands/GenerateServiceTemplates.php @@ -0,0 +1,96 @@ +clearAll(); + $files = array_diff(scandir(base_path('templates/compose')), ['.', '..']); + $files = array_filter($files, function ($file) { + return strpos($file, '.yaml') !== false; + }); + $serviceTemplatesJson = []; + foreach ($files as $file) { + $parsed = $this->process_file($file); + if ($parsed) { + $name = data_get($parsed, 'name'); + $parsed = data_forget($parsed, 'name'); + $serviceTemplatesJson[$name] = $parsed; + } + } + $serviceTemplatesJson = json_encode($serviceTemplatesJson, JSON_PRETTY_PRINT); + file_put_contents(base_path('templates/service-templates.json'), $serviceTemplatesJson); + } + + private function process_file($file) + { + $serviceName = str($file)->before('.yaml')->value(); + $content = file_get_contents(base_path("templates/compose/$file")); + // $this->info($content); + $ignore = collect(preg_grep('/^# ignore:/', explode("\n", $content)))->values(); + if ($ignore->count() > 0) { + $ignore = (bool)str($ignore[0])->after('# ignore:')->trim()->value(); + } else { + $ignore = false; + } + if ($ignore) { + $this->info("Ignoring $file"); + return; + } + $this->info("Processing $file"); + $documentation = collect(preg_grep('/^# documentation:/', explode("\n", $content)))->values(); + if ($documentation->count() > 0) { + $documentation = str($documentation[0])->after('# documentation:')->trim()->value(); + } else { + $documentation = 'https://coolify.io/docs'; + } + + $slogan = collect(preg_grep('/^# slogan:/', explode("\n", $content)))->values(); + if ($slogan->count() > 0) { + $slogan = str($slogan[0])->after('# slogan:')->trim()->value(); + } else { + $slogan = str($file)->headline()->value(); + } + $env_file = collect(preg_grep('/^# env_file:/', explode("\n", $content)))->values(); + if ($env_file->count() > 0) { + $env_file = str($env_file[0])->after('# env_file:')->trim()->value(); + } else { + $env_file = null; + } + + $json = Yaml::parse($content); + $yaml = base64_encode(Yaml::dump($json, 10, 2)); + $payload = [ + 'name' => $serviceName, + 'documentation' => $documentation, + 'slogan' => $slogan, + 'compose' => $yaml, + ]; + if ($env_file) { + $payload['envs'] = $env_file; + } + return $payload; + } +} diff --git a/app/Http/Controllers/ProjectController.php b/app/Http/Controllers/ProjectController.php index 7e67a588c..1d1a5b14e 100644 --- a/app/Http/Controllers/ProjectController.php +++ b/app/Http/Controllers/ProjectController.php @@ -63,6 +63,8 @@ class ProjectController extends Controller $database = create_standalone_postgresql($environment->id, $destination_uuid); } else if ($type->value() === 'redis') { $database = create_standalone_redis($environment->id, $destination_uuid); + } else if ($type->value() === 'mongodb') { + $database = create_standalone_mongodb($environment->id, $destination_uuid); } return redirect()->route('project.database.configuration', [ 'project_uuid' => $project->uuid, diff --git a/app/Http/Livewire/Boarding/Index.php b/app/Http/Livewire/Boarding/Index.php index 53065918a..1b9093be6 100644 --- a/app/Http/Livewire/Boarding/Index.php +++ b/app/Http/Livewire/Boarding/Index.php @@ -213,7 +213,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== ]); $this->getProxyType(); } catch (\Throwable $e) { - $this->dockerInstallationStarted = false; + // $this->dockerInstallationStarted = false; return handleError(error: $e, customErrorMessage: $customErrorMessage, livewire: $this); } } diff --git a/app/Http/Livewire/Dashboard.php b/app/Http/Livewire/Dashboard.php index b46df481a..723b00f7f 100644 --- a/app/Http/Livewire/Dashboard.php +++ b/app/Http/Livewire/Dashboard.php @@ -17,10 +17,6 @@ class Dashboard extends Component $this->servers = Server::ownedByCurrentTeam()->get(); $this->projects = Project::ownedByCurrentTeam()->get(); } - // public function createToken() { - // $token = auth()->user()->createToken('test'); - // ray($token); - // } // public function getIptables() // { // $servers = Server::ownedByCurrentTeam()->get(); diff --git a/app/Http/Livewire/Project/Application/General.php b/app/Http/Livewire/Project/Application/General.php index b4ebfff5d..c9c0c77ee 100644 --- a/app/Http/Livewire/Project/Application/General.php +++ b/app/Http/Livewire/Project/Application/General.php @@ -19,6 +19,7 @@ class General extends Component public string $git_branch; public ?string $git_commit_sha = null; public string $build_pack; + public ?string $ports_exposes = null; public $customLabels; public bool $labelsChanged = false; @@ -80,6 +81,7 @@ class General extends Component public function mount() { + $this->ports_exposes = $this->application->ports_exposes; if (str($this->application->status)->startsWith('running') && is_null($this->application->config_hash)) { $this->application->isConfigurationChanged(true); } @@ -156,6 +158,7 @@ class General extends Component public function resetDefaultLabels($showToaster = true) { $this->customLabels = str(implode(",", generateLabelsApplication($this->application)))->replace(',', "\n"); + $this->ports_exposes = $this->application->ports_exposes; $this->submit($showToaster); } @@ -168,6 +171,9 @@ class General extends Component { try { $this->validate(); + if ($this->ports_exposes !== $this->application->ports_exposes) { + $this->resetDefaultLabels(false); + } if (data_get($this->application, 'build_pack') === 'dockerimage') { $this->validate([ 'application.docker_registry_image_name' => 'required', diff --git a/app/Http/Livewire/Project/CloneProject.php b/app/Http/Livewire/Project/CloneProject.php new file mode 100644 index 000000000..b271a7970 --- /dev/null +++ b/app/Http/Livewire/Project/CloneProject.php @@ -0,0 +1,143 @@ + 'Please select a server.', + 'newProjectName' => 'Please enter a name for the new project.', + ]; + public function mount($project_uuid) + { + $this->project_uuid = $project_uuid; + $this->project = Project::where('uuid', $project_uuid)->firstOrFail(); + $this->environment = $this->project->environments->where('name', $this->environment_name)->first(); + $this->project_id = $this->project->id; + $this->servers = currentTeam()->servers; + $this->newProjectName = $this->project->name . ' (clone)'; + } + + public function render() + { + return view('livewire.project.clone-project'); + } + + public function selectServer($server_id) + { + $this->selectedServer = $server_id; + $this->server = $this->servers->where('id', $server_id)->first(); + } + + public function clone() + { + try { + $this->validate([ + 'selectedServer' => 'required', + 'newProjectName' => 'required', + ]); + $newProject = Project::create([ + 'name' => $this->newProjectName, + 'team_id' => currentTeam()->id, + 'description' => $this->project->description . ' (clone)', + ]); + if ($this->environment->id !== 1) { + $newProject->environments()->create([ + 'name' => $this->environment->name, + ]); + $newProject->environments()->find(1)->delete(); + } + $newEnvironment = $newProject->environments->first(); + // Clone Applications + $applications = $this->environment->applications; + $databases = $this->environment->databases(); + $services = $this->environment->services; + foreach ($applications as $application) { + $uuid = (string)new Cuid2(7); + $newApplication = $application->replicate()->fill([ + 'uuid' => $uuid, + 'fqdn' => generateFqdn($this->server, $uuid), + 'status' => 'exited', + 'environment_id' => $newEnvironment->id, + 'destination_id' => $this->selectedServer, + ]); + $newApplication->environment_id = $newProject->environments->first()->id; + $newApplication->save(); + $environmentVaribles = $application->environment_variables()->get(); + foreach ($environmentVaribles as $environmentVarible) { + $newEnvironmentVariable = $environmentVarible->replicate()->fill([ + 'application_id' => $newApplication->id, + ]); + $newEnvironmentVariable->save(); + } + $persistentVolumes = $application->persistentStorages()->get(); + foreach ($persistentVolumes as $volume) { + $newPersistentVolume = $volume->replicate()->fill([ + 'name' => $newApplication->uuid . '-' . str($volume->name)->afterLast('-'), + 'resource_id' => $newApplication->id, + ]); + $newPersistentVolume->save(); + } + } + foreach ($databases as $database) { + $uuid = (string)new Cuid2(7); + $newDatabase = $database->replicate()->fill([ + 'uuid' => $uuid, + 'environment_id' => $newEnvironment->id, + 'destination_id' => $this->selectedServer, + ]); + $newDatabase->environment_id = $newProject->environments->first()->id; + $newDatabase->save(); + $environmentVaribles = $database->environment_variables()->get(); + foreach ($environmentVaribles as $environmentVarible) { + $payload = []; + if ($database->type() === 'standalone-postgres') { + $payload['standalone_postgresql_id'] = $newDatabase->id; + } else if ($database->type() === 'standalone_redis') { + $payload['standalone_redis_id'] = $newDatabase->id; + } else if ($database->type() === 'standalone_mongodb') { + $payload['standalone_mongodb_id'] = $newDatabase->id; + } + $newEnvironmentVariable = $environmentVarible->replicate()->fill($payload); + $newEnvironmentVariable->save(); + } + } + foreach ($services as $service) { + $uuid = (string)new Cuid2(7); + $newService = $service->replicate()->fill([ + 'uuid' => $uuid, + 'environment_id' => $newEnvironment->id, + 'destination_id' => $this->selectedServer, + ]); + $newService->environment_id = $newProject->environments->first()->id; + $newService->save(); + $newService->parse(); + } + return redirect()->route('project.resources', [ + 'project_uuid' => $newProject->uuid, + 'environment_name' => $newEnvironment->name, + ]); + } catch (\Exception $e) { + return handleError($e, $this); + } + } +} diff --git a/app/Http/Livewire/Project/Database/CreateScheduledBackup.php b/app/Http/Livewire/Project/Database/CreateScheduledBackup.php index 35ecebdd3..ac34e93bd 100644 --- a/app/Http/Livewire/Project/Database/CreateScheduledBackup.php +++ b/app/Http/Livewire/Project/Database/CreateScheduledBackup.php @@ -22,6 +22,11 @@ class CreateScheduledBackup extends Component 'frequency' => 'Backup Frequency', 'save_s3' => 'Save to S3', ]; + public function mount() { + if ($this->s3s->count() > 0) { + $this->s3_storage_id = $this->s3s->first()->id; + } + } public function submit(): void { diff --git a/app/Http/Livewire/Project/Database/Heading.php b/app/Http/Livewire/Project/Database/Heading.php index fc867ce79..6045e2b7f 100644 --- a/app/Http/Livewire/Project/Database/Heading.php +++ b/app/Http/Livewire/Project/Database/Heading.php @@ -2,6 +2,7 @@ namespace App\Http\Livewire\Project\Database; +use App\Actions\Database\StartMongodb; use App\Actions\Database\StartPostgresql; use App\Actions\Database\StartRedis; use App\Actions\Database\StopDatabase; @@ -46,11 +47,15 @@ class Heading extends Component public function start() { if ($this->database->type() === 'standalone-postgresql') { - $activity = StartPostgresql::run($this->database->destination->server, $this->database); + $activity = StartPostgresql::run($this->database); $this->emit('newMonitorActivity', $activity->id); } if ($this->database->type() === 'standalone-redis') { - $activity = StartRedis::run($this->database->destination->server, $this->database); + $activity = StartRedis::run($this->database); + $this->emit('newMonitorActivity', $activity->id); + } + if ($this->database->type() === 'standalone-mongodb') { + $activity = StartMongodb::run($this->database); $this->emit('newMonitorActivity', $activity->id); } } diff --git a/app/Http/Livewire/Project/Database/Mongodb/General.php b/app/Http/Livewire/Project/Database/Mongodb/General.php new file mode 100644 index 000000000..e0fc3c277 --- /dev/null +++ b/app/Http/Livewire/Project/Database/Mongodb/General.php @@ -0,0 +1,91 @@ + 'required', + 'database.description' => 'nullable', + 'database.mongo_conf' => 'nullable', + 'database.mongo_initdb_root_username' => 'required', + 'database.mongo_initdb_root_password' => 'required', + 'database.mongo_initdb_database' => 'required', + 'database.image' => 'required', + 'database.ports_mappings' => 'nullable', + 'database.is_public' => 'nullable|boolean', + 'database.public_port' => 'nullable|integer', + ]; + protected $validationAttributes = [ + 'database.name' => 'Name', + 'database.description' => 'Description', + 'database.mongo_conf' => 'Mongo Configuration', + 'database.mongo_initdb_root_username' => 'Root Username', + 'database.mongo_initdb_root_password' => 'Root Password', + 'database.mongo_initdb_database' => 'Database', + 'database.image' => 'Image', + 'database.ports_mappings' => 'Port Mapping', + 'database.is_public' => 'Is Public', + 'database.public_port' => 'Public Port', + ]; + public function submit() { + try { + $this->validate(); + if ($this->database->mongo_conf === "") { + $this->database->mongo_conf = null; + } + $this->database->save(); + $this->emit('success', 'Database updated successfully.'); + } catch (Exception $e) { + return handleError($e, $this); + } + } + public function instantSave() + { + try { + if ($this->database->is_public && !$this->database->public_port) { + $this->emit('error', 'Public port is required.'); + $this->database->is_public = false; + return; + } + if ($this->database->is_public) { + $this->emit('success', 'Starting TCP proxy...'); + StartDatabaseProxy::run($this->database); + $this->emit('success', 'Database is now publicly accessible.'); + } else { + StopDatabaseProxy::run($this->database); + $this->emit('success', 'Database is no longer publicly accessible.'); + } + $this->db_url = $this->database->getDbUrl(); + $this->database->save(); + } catch(\Throwable $e) { + $this->database->is_public = !$this->database->is_public; + return handleError($e, $this); + } + } + public function refresh(): void + { + $this->database->refresh(); + } + + public function mount() + { + $this->db_url = $this->database->getDbUrl(); + } + + public function render() + { + return view('livewire.project.database.mongodb.general'); + } +} diff --git a/app/Http/Livewire/Project/Database/Postgresql/General.php b/app/Http/Livewire/Project/Database/Postgresql/General.php index 8c2867e7f..df1f0da85 100644 --- a/app/Http/Livewire/Project/Database/Postgresql/General.php +++ b/app/Http/Livewire/Project/Database/Postgresql/General.php @@ -49,15 +49,7 @@ class General extends Component ]; public function mount() { - $this->getDbUrl(); - } - public function getDbUrl() { - - if ($this->database->is_public) { - $this->db_url = "postgres://{$this->database->postgres_user}:{$this->database->postgres_password}@{$this->database->destination->server->getIp}:{$this->database->public_port}/{$this->database->postgres_db}"; - } else { - $this->db_url = "postgres://{$this->database->postgres_user}:{$this->database->postgres_password}@{$this->database->uuid}:5432/{$this->database->postgres_db}"; - } + $this->db_url = $this->database->getDbUrl(); } public function instantSave() { @@ -75,7 +67,7 @@ class General extends Component StopDatabaseProxy::run($this->database); $this->emit('success', 'Database is no longer publicly accessible.'); } - $this->getDbUrl(); + $this->db_url = $this->database->getDbUrl(); $this->database->save(); } catch(\Throwable $e) { $this->database->is_public = !$this->database->is_public; diff --git a/app/Http/Livewire/Project/Database/Redis/General.php b/app/Http/Livewire/Project/Database/Redis/General.php index c0d3b2f0b..6f33ae30a 100644 --- a/app/Http/Livewire/Project/Database/Redis/General.php +++ b/app/Http/Livewire/Project/Database/Redis/General.php @@ -63,7 +63,7 @@ class General extends Component StopDatabaseProxy::run($this->database); $this->emit('success', 'Database is no longer publicly accessible.'); } - $this->getDbUrl(); + $this->db_url = $this->database->getDbUrl(); $this->database->save(); } catch(\Throwable $e) { $this->database->is_public = !$this->database->is_public; @@ -77,15 +77,7 @@ class General extends Component public function mount() { - $this->getDbUrl(); - } - public function getDbUrl() { - - if ($this->database->is_public) { - $this->db_url = "redis://:{$this->database->redis_password}@{$this->database->destination->server->getIp}:{$this->database->public_port}/0"; - } else { - $this->db_url = "redis://:{$this->database->redis_password}@{$this->database->uuid}:6379/0"; - } + $this->db_url = $this->database->getDbUrl(); } public function render() { diff --git a/app/Http/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Http/Livewire/Project/Shared/EnvironmentVariable/All.php index ce7c5ae40..f453b4bf3 100644 --- a/app/Http/Livewire/Project/Shared/EnvironmentVariable/All.php +++ b/app/Http/Livewire/Project/Shared/EnvironmentVariable/All.php @@ -78,6 +78,9 @@ class All extends Component case 'standalone-redis': $environment->standalone_redis_id = $this->resource->id; break; + case 'standalone-mongodb': + $environment->standalone_mongodb_id = $this->resource->id; + break; case 'service': $environment->service_id = $this->resource->id; break; diff --git a/app/Http/Livewire/Project/Shared/GetLogs.php b/app/Http/Livewire/Project/Shared/GetLogs.php index cd0ee98fd..90983785d 100644 --- a/app/Http/Livewire/Project/Shared/GetLogs.php +++ b/app/Http/Livewire/Project/Shared/GetLogs.php @@ -13,6 +13,7 @@ class GetLogs extends Component public Server $server; public ?string $container = null; public ?bool $streamLogs = false; + public ?bool $showTimeStamps = true; public int $numberOfLines = 100; public function doSomethingWithThisChunkOfOutput($output) { @@ -24,7 +25,11 @@ class GetLogs extends Component public function getLogs($refresh = false) { if ($this->container) { - $sshCommand = generateSshCommand($this->server, "docker logs -n {$this->numberOfLines} -t {$this->container}"); + if ($this->showTimeStamps) { + $sshCommand = generateSshCommand($this->server, "docker logs -n {$this->numberOfLines} -t {$this->container}"); + } else { + $sshCommand = generateSshCommand($this->server, "docker logs -n {$this->numberOfLines} {$this->container}"); + } if ($refresh) { $this->outputs = ''; } diff --git a/app/Http/Livewire/Project/Shared/Logs.php b/app/Http/Livewire/Project/Shared/Logs.php index 15a4e510c..80cdf82c4 100644 --- a/app/Http/Livewire/Project/Shared/Logs.php +++ b/app/Http/Livewire/Project/Shared/Logs.php @@ -5,6 +5,7 @@ namespace App\Http\Livewire\Project\Shared; use App\Models\Application; use App\Models\Server; use App\Models\Service; +use App\Models\StandaloneMongodb; use App\Models\StandalonePostgresql; use App\Models\StandaloneRedis; use Livewire\Component; @@ -12,7 +13,7 @@ use Livewire\Component; class Logs extends Component { public ?string $type = null; - public Application|StandalonePostgresql|Service|StandaloneRedis $resource; + public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb $resource; public Server $server; public ?string $container = null; public $parameters; @@ -38,9 +39,13 @@ class Logs extends Component if (is_null($resource)) { $resource = StandaloneRedis::where('uuid', $this->parameters['database_uuid'])->first(); if (is_null($resource)) { - abort(404); + $resource = StandaloneMongodb::where('uuid', $this->parameters['database_uuid'])->first(); + if (is_null($resource)) { + abort(404); + } } } + $this->resource = $resource; $this->status = $this->resource->status; $this->server = $this->resource->destination->server; diff --git a/app/Http/Livewire/Security/ApiTokens.php b/app/Http/Livewire/Security/ApiTokens.php new file mode 100644 index 000000000..f00b30930 --- /dev/null +++ b/app/Http/Livewire/Security/ApiTokens.php @@ -0,0 +1,38 @@ +tokens = auth()->user()->tokens; + } + public function addNewToken() + { + try { + $this->validate([ + 'description' => 'required|min:3|max:255', + ]); + $token = auth()->user()->createToken($this->description); + $this->tokens = auth()->user()->tokens; + session()->flash('token', $token->plainTextToken); + } catch (\Exception $e) { + return handleError($e, $this); + } + } + public function revoke(int $id) + { + $token = auth()->user()->tokens()->where('id', $id)->first(); + $token->delete(); + $this->tokens = auth()->user()->tokens; + } +} diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index 742ed9cee..9104434ea 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -6,6 +6,7 @@ use App\Models\S3Storage; use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackupExecution; use App\Models\Server; +use App\Models\StandaloneMongodb; use App\Models\StandalonePostgresql; use App\Models\Team; use App\Notifications\Database\BackupFailed; @@ -27,7 +28,7 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted public ?Team $team = null; public Server $server; public ScheduledDatabaseBackup $backup; - public StandalonePostgresql $database; + public StandalonePostgresql|StandaloneMongodb $database; public ?string $container_name = null; public ?ScheduledDatabaseBackupExecution $backup_log = null; @@ -72,12 +73,24 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted if (is_null($databasesToBackup)) { if ($databaseType === 'standalone-postgresql') { $databasesToBackup = [$this->database->postgres_db]; + } else if ($databaseType === 'standalone-mongodb') { + $databasesToBackup = ['*']; } else { return; } } else { - $databasesToBackup = explode(',', $databasesToBackup); - $databasesToBackup = array_map('trim', $databasesToBackup); + if ($databaseType === 'standalone-postgresql') { + // Format: db1,db2,db3 + $databasesToBackup = explode(',', $databasesToBackup); + $databasesToBackup = array_map('trim', $databasesToBackup); + } else if ($databaseType === 'standalone-mongodb') { + // Format: db1:collection1,collection2|db2:collection3,collection4 + $databasesToBackup = explode('|', $databasesToBackup); + $databasesToBackup = array_map('trim', $databasesToBackup); + ray($databasesToBackup); + } else { + return; + } } $this->container_name = $this->database->uuid; $this->backup_dir = backup_dir() . "/databases/" . Str::of($this->team->name)->slug() . '-' . $this->team->id . '/' . $this->container_name; @@ -92,15 +105,37 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted $size = 0; ray('Backing up ' . $database); try { - $this->backup_file = "/pg-dump-$database-" . Carbon::now()->timestamp . ".dmp"; - $this->backup_location = $this->backup_dir . $this->backup_file; - $this->backup_log = ScheduledDatabaseBackupExecution::create([ - 'database_name' => $database, - 'filename' => $this->backup_location, - 'scheduled_database_backup_id' => $this->backup->id, - ]); if ($databaseType === 'standalone-postgresql') { + $this->backup_file = "/pg-dump-$database-" . Carbon::now()->timestamp . ".dmp"; + $this->backup_location = $this->backup_dir . $this->backup_file; + $this->backup_log = ScheduledDatabaseBackupExecution::create([ + 'database_name' => $database, + 'filename' => $this->backup_location, + 'scheduled_database_backup_id' => $this->backup->id, + ]); $this->backup_standalone_postgresql($database); + } else if ($databaseType === 'standalone-mongodb') { + if ($database === '*') { + $database = 'all'; + $databaseName = 'all'; + } else { + if (str($database)->contains(':')) { + $databaseName = str($database)->before(':'); + } else { + $databaseName = $database; + } + ray($databaseName); + } + $this->backup_file = "/mongo-dump-$databaseName-" . Carbon::now()->timestamp . ".tar.gz"; + $this->backup_location = $this->backup_dir . $this->backup_file; + $this->backup_log = ScheduledDatabaseBackupExecution::create([ + 'database_name' => $databaseName, + 'filename' => $this->backup_location, + 'scheduled_database_backup_id' => $this->backup->id, + ]); + $this->backup_standalone_mongodb($database); + } else { + throw new \Exception('Unsupported database type'); } $size = $this->calculate_size(); $this->remove_old_backups(); @@ -114,12 +149,14 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted 'size' => $size, ]); } catch (\Throwable $e) { - $this->backup_log->update([ - 'status' => 'failed', - 'message' => $this->backup_output, - 'size' => $size, - 'filename' => null - ]); + if ($this->backup_log) { + $this->backup_log->update([ + 'status' => 'failed', + 'message' => $this->backup_output, + 'size' => $size, + 'filename' => null + ]); + } send_internal_notification('DatabaseBackupJob failed with: ' . $e->getMessage()); $this->team->notify(new BackupFailed($this->backup, $this->database, $this->backup_output)); throw $e; @@ -130,11 +167,36 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted throw $e; } } + private function backup_standalone_mongodb(string $databaseWithCollections): void + { + try { + $url = $this->database->getDbUrl(); + if ($databaseWithCollections === 'all') { + $commands[] = "mkdir -p " . $this->backup_dir; + $commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --gzip --archive > $this->backup_location"; + } else { + $collectionsToExclude = str($databaseWithCollections)->after(':')->explode(','); + $databaseName = str($databaseWithCollections)->before(':'); + $commands[] = "mkdir -p " . $this->backup_dir; + $commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --db $databaseName --gzip --excludeCollection " . $collectionsToExclude->implode(' --excludeCollection ') . " --archive > $this->backup_location"; + } + ray($commands); + $this->backup_output = instant_remote_process($commands, $this->server); + $this->backup_output = trim($this->backup_output); + if ($this->backup_output === '') { + $this->backup_output = null; + } + ray('Backup done for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location); + } catch (\Throwable $e) { + $this->add_to_backup_output($e->getMessage()); + ray('Backup failed for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location . '\n\nError:' . $e->getMessage()); + throw $e; + } + } private function backup_standalone_postgresql(string $database): void { try { - ray($this->backup_dir); $commands[] = "mkdir -p " . $this->backup_dir; $commands[] = "docker exec $this->container_name pg_dump --format=custom --no-acl --no-owner --username {$this->database->postgres_user} $database > $this->backup_location"; $this->backup_output = instant_remote_process($commands, $this->server); @@ -189,11 +251,7 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted $bucket = $this->s3->bucket; $endpoint = $this->s3->endpoint; $this->s3->testConnection(); - if (isDev()) { - $commands[] = "docker run --pull=always -d --network {$this->database->destination->network} --name backup-of-{$this->backup->uuid} --rm -v coolify_coolify-data-dev:/data/coolify:ro ghcr.io/coollabsio/coolify-helper >/dev/null 2>&1"; - } else { - $commands[] = "docker run --pull=always -d --network {$this->database->destination->network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro ghcr.io/coollabsio/coolify-helper >/dev/null 2>&1"; - } + $commands[] = "docker run --pull=always -d --network {$this->database->destination->network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro ghcr.io/coollabsio/coolify-helper >/dev/null 2>&1"; $commands[] = "docker exec backup-of-{$this->backup->uuid} mc config host add temporary {$endpoint} $key $secret"; $commands[] = "docker exec backup-of-{$this->backup->uuid} mc cp $this->backup_location temporary/$bucket{$this->backup_dir}/"; diff --git a/app/Jobs/StopResourceJob.php b/app/Jobs/StopResourceJob.php index 0bfc1f2fa..721f7f698 100644 --- a/app/Jobs/StopResourceJob.php +++ b/app/Jobs/StopResourceJob.php @@ -7,6 +7,7 @@ use App\Actions\Database\StopDatabase; use App\Actions\Service\StopService; use App\Models\Application; use App\Models\Service; +use App\Models\StandaloneMongodb; use App\Models\StandalonePostgresql; use App\Models\StandaloneRedis; use Illuminate\Bus\Queueable; @@ -20,7 +21,7 @@ class StopResourceJob implements ShouldQueue, ShouldBeEncrypted { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public function __construct(public Application|Service|StandalonePostgresql|StandaloneRedis $resource) + public function __construct(public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb $resource) { } @@ -41,6 +42,9 @@ class StopResourceJob implements ShouldQueue, ShouldBeEncrypted case 'standalone-redis': StopDatabase::run($this->resource); break; + case 'standalone-mongodb': + StopDatabase::run($this->resource); + break; case 'service': StopService::run($this->resource); break; diff --git a/app/Models/Environment.php b/app/Models/Environment.php index f66bf48f2..8f67ed004 100644 --- a/app/Models/Environment.php +++ b/app/Models/Environment.php @@ -14,7 +14,11 @@ class Environment extends Model public function can_delete_environment() { - return $this->applications()->count() == 0 && $this->redis()->count() == 0 && $this->postgresqls()->count() == 0 && $this->services()->count() == 0; + return $this->applications()->count() == 0 && + $this->redis()->count() == 0 && + $this->postgresqls()->count() == 0 && + $this->mongodbs()->count() == 0 && + $this->services()->count() == 0; } public function applications() @@ -30,12 +34,17 @@ class Environment extends Model { return $this->hasMany(StandaloneRedis::class); } + public function mongodbs() + { + return $this->hasMany(StandaloneMongodb::class); + } public function databases() { $postgresqls = $this->postgresqls; $redis = $this->redis; - return $postgresqls->concat($redis); + $mongodbs = $this->mongodbs; + return $postgresqls->concat($redis)->concat($mongodbs); } public function project() diff --git a/app/Models/Server.php b/app/Models/Server.php index 07f975c48..7ff517ef6 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -122,9 +122,10 @@ class Server extends BaseModel public function databases() { return $this->destinations()->map(function ($standaloneDocker) { - $postgresqls = $standaloneDocker->postgresqls; - $redis = $standaloneDocker->redis; - return $postgresqls->concat($redis); + $postgresqls = data_get($standaloneDocker,'postgresqls',collect([])); + $redis = data_get($standaloneDocker,'redis',collect([])); + $mongodbs = data_get($standaloneDocker,'mongodbs',collect([])); + return $postgresqls->concat($redis)->concat($mongodbs); })->flatten(); } public function applications() diff --git a/app/Models/StandaloneDocker.php b/app/Models/StandaloneDocker.php index a594b854a..9e70b7514 100644 --- a/app/Models/StandaloneDocker.php +++ b/app/Models/StandaloneDocker.php @@ -15,10 +15,15 @@ class StandaloneDocker extends BaseModel { return $this->morphMany(StandalonePostgresql::class, 'destination'); } + public function redis() { return $this->morphMany(StandaloneRedis::class, 'destination'); } + public function mongodbs() + { + return $this->morphMany(StandaloneMongodb::class, 'destination'); + } public function server() { diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php new file mode 100644 index 000000000..56c644481 --- /dev/null +++ b/app/Models/StandaloneMongodb.php @@ -0,0 +1,99 @@ + 'mongodb-data-' . $database->uuid, + 'mount_path' => '/data', + 'host_path' => null, + 'resource_id' => $database->id, + 'resource_type' => $database->getMorphClass(), + 'is_readonly' => true + ]); + }); + static::deleting(function ($database) { + $database->scheduledBackups()->delete(); + $storages = $database->persistentStorages()->get(); + foreach ($storages as $storage) { + instant_remote_process(["docker volume rm -f $storage->name"], $database->destination->server, false); + } + $database->persistentStorages()->delete(); + $database->environment_variables()->delete(); + }); + } + + public function portsMappings(): Attribute + { + return Attribute::make( + set: fn ($value) => $value === "" ? null : $value, + ); + } + + public function portsMappingsArray(): Attribute + { + return Attribute::make( + get: fn () => is_null($this->ports_mappings) + ? [] + : explode(',', $this->ports_mappings), + + ); + } + + public function type(): string + { + return 'standalone-mongodb'; + } + public function getDbUrl() { + if ($this->is_public) { + return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->destination->server->getIp}:{$this->public_port}/?directConnection=true"; + } else { + return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->uuid}:27017/?directConnection=true"; + } + } + 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/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php index 41a10cfd9..669b43f58 100644 --- a/app/Models/StandalonePostgresql.php +++ b/app/Models/StandalonePostgresql.php @@ -62,6 +62,14 @@ class StandalonePostgresql extends BaseModel { return 'standalone-postgresql'; } + public function getDbUrl(): string + { + if ($this->is_public) { + return "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->postgres_db}"; + } else { + return "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->uuid}:5432/{$this->postgres_db}"; + } + } public function environment() { diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php index 6517c2ef7..9dff7ae84 100644 --- a/app/Models/StandaloneRedis.php +++ b/app/Models/StandaloneRedis.php @@ -41,8 +41,6 @@ class StandaloneRedis extends BaseModel ); } - // Normal Deployments - public function portsMappingsArray(): Attribute { return Attribute::make( @@ -57,6 +55,13 @@ class StandaloneRedis extends BaseModel { return 'standalone-redis'; } + public function getDbUrl(): string { + if ($this->is_public) { + return "redis://:{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0"; + } else { + return "redis://:{$this->redis_password}@{$this->uuid}:6379/0"; + } + } public function environment() { diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index f78ed363f..f60994c61 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -46,9 +46,9 @@ class RouteServiceProvider extends ServiceProvider { RateLimiter::for('api', function (Request $request) { if ($request->path() === 'api/health') { - return Limit::perMinute(5000)->by($request->user()?->id ?: $request->ip()); + return Limit::perMinute(1000)->by($request->user()?->id ?: $request->ip()); } - return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip()); + return Limit::perMinute(30)->by($request->user()?->id ?: $request->ip()); }); RateLimiter::for('5', function (Request $request) { return Limit::perMinute(5)->by($request->user()?->id ?: $request->ip()); diff --git a/bootstrap/helpers/constants.php b/bootstrap/helpers/constants.php index f915ea041..586ba531d 100644 --- a/bootstrap/helpers/constants.php +++ b/bootstrap/helpers/constants.php @@ -1,6 +1,6 @@ '* * * * *', 'hourly' => '0 * * * *', diff --git a/bootstrap/helpers/databases.php b/bootstrap/helpers/databases.php index 3c4f0dfd9..0c5c8898e 100644 --- a/bootstrap/helpers/databases.php +++ b/bootstrap/helpers/databases.php @@ -2,6 +2,7 @@ use App\Models\Server; use App\Models\StandaloneDocker; +use App\Models\StandaloneMongodb; use App\Models\StandalonePostgresql; use App\Models\StandaloneRedis; use Visus\Cuid2\Cuid2; @@ -43,6 +44,21 @@ function create_standalone_redis($environment_id, $destination_uuid): Standalone ]); } +function create_standalone_mongodb($environment_id, $destination_uuid): StandaloneMongodb +{ + $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); + if (!$destination) { + throw new Exception('Destination not found'); + } + return StandaloneMongodb::create([ + 'name' => generate_database_name('mongodb'), + 'mongo_initdb_root_password' => \Illuminate\Support\Str::password(symbols: false), + 'environment_id' => $environment_id, + 'destination_id' => $destination->id, + 'destination_type' => $destination->getMorphClass(), + ]); +} + /** * Delete file locally on the filesystem. * @param string $filename diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 25237abab..3348ce1ae 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -1,7 +1,12 @@ sortKeys(); - $deprecated = File::get(base_path('templates/deprecated.json')); - $deprecated = collect(json_decode($deprecated))->sortKeys(); - $services = $services->merge($deprecated); $version = config('version'); $services = $services->map(function ($service) use ($version) { if (version_compare($version, data_get($service, 'minVersion', '0.0.0'), '<')) { @@ -456,3 +458,31 @@ function getServiceTemplates() } return $services; } + +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; + } + return null; + } else { + return $resource; + } +} +function queryResourcesByUuid(string $uuid) +{ + $resource = null; + $application = Application::whereUuid($uuid)->first(); + if ($application) return $application; + $service = Service::whereUuid($uuid)->first(); + if ($service) return $service; + $postgresql = StandalonePostgresql::whereUuid($uuid)->first(); + if ($postgresql) return $postgresql; + $redis = StandaloneRedis::whereUuid($uuid)->first(); + if ($redis) return $redis; + $mongodb = StandaloneMongodb::whereUuid($uuid)->first(); + if ($mongodb) return $mongodb; + return $resource; +} diff --git a/config/sentry.php b/config/sentry.php index 1c75b9d50..a7d0a2a84 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -7,7 +7,7 @@ return [ // 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.97', + 'release' => '4.0.0-beta.98', // 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 af0296c72..36134c947 100644 --- a/config/version.php +++ b/config/version.php @@ -1,3 +1,3 @@ + */ +class StandaloneMongodbFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + // + ]; + } +} diff --git a/database/migrations/2023_10_19_101331_create_standalone_mongodbs_table.php b/database/migrations/2023_10_19_101331_create_standalone_mongodbs_table.php new file mode 100644 index 000000000..30f5c24af --- /dev/null +++ b/database/migrations/2023_10_19_101331_create_standalone_mongodbs_table.php @@ -0,0 +1,56 @@ +id(); + $table->string('uuid')->unique(); + $table->string('name'); + $table->string('description')->nullable(); + + $table->text('mongo_conf')->nullable(); + $table->text('mongo_initdb_root_username')->default('root'); + $table->text('mongo_initdb_root_password'); + $table->text('mongo_initdb_database')->default('default'); + + $table->string('status')->default('exited'); + + $table->string('image')->default('mongo:7'); + + $table->boolean('is_public')->default(false); + $table->integer('public_port')->nullable(); + $table->text('ports_mappings')->nullable(); + + $table->string('limits_memory')->default("0"); + $table->string('limits_memory_swap')->default("0"); + $table->integer('limits_memory_swappiness')->default(60); + $table->string('limits_memory_reservation')->default("0"); + + $table->string('limits_cpus')->default("0"); + $table->string('limits_cpuset')->nullable()->default("0"); + $table->integer('limits_cpu_shares')->default(1024); + + $table->timestamp('started_at')->nullable(); + $table->morphs('destination'); + $table->foreignId('environment_id')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('standalone_mongodbs'); + } +}; diff --git a/database/migrations/2023_10_19_101332_add_standalone_mongodb_to_environment_variables_table.php b/database/migrations/2023_10_19_101332_add_standalone_mongodb_to_environment_variables_table.php new file mode 100644 index 000000000..b67c22637 --- /dev/null +++ b/database/migrations/2023_10_19_101332_add_standalone_mongodb_to_environment_variables_table.php @@ -0,0 +1,28 @@ +foreignId('standalone_mongodb_id')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('environment_variables', function (Blueprint $table) { + $table->dropColumn('standalone_mongodb_id'); + }); + } +}; diff --git a/database/seeders/ApplicationSeeder.php b/database/seeders/ApplicationSeeder.php index 2d2d00cb7..54863f2d6 100644 --- a/database/seeders/ApplicationSeeder.php +++ b/database/seeders/ApplicationSeeder.php @@ -37,7 +37,7 @@ class ApplicationSeeder extends Seeder 'git_repository' => 'coollabsio/coolify-examples', 'git_branch' => 'dockerfile', 'build_pack' => 'dockerfile', - 'ports_exposes' => '3000', + 'ports_exposes' => '80', 'environment_id' => 1, 'destination_id' => 0, 'destination_type' => StandaloneDocker::class, diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 0bf3e8828..9b7af1c02 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -34,7 +34,7 @@ services: POSTGRES_DB: "${DB_DATABASE:-coolify}" POSTGRES_HOST_AUTH_METHOD: "trust" volumes: - - ./_data/coolify/_volumes/database/:/var/lib/postgresql/data + - /data/coolify/_volumes/database/:/var/lib/postgresql/data # - coolify-pg-data-dev:/var/lib/postgresql/data redis: ports: @@ -42,7 +42,7 @@ services: env_file: - .env volumes: - - ./_data/coolify/_volumes/redis/:/data + - /data/coolify/_volumes/redis/:/data # - coolify-redis-data-dev:/data vite: image: node:19 @@ -58,7 +58,7 @@ services: volumes: - /:/host - /var/run/docker.sock:/var/run/docker.sock - - ./_data/coolify/:/data/coolify + - /data/coolify/:/data/coolify # - coolify-data-dev:/data/coolify mailpit: image: "axllent/mailpit:latest" @@ -79,7 +79,7 @@ services: MINIO_ACCESS_KEY: "${MINIO_ACCESS_KEY:-minioadmin}" MINIO_SECRET_KEY: "${MINIO_SECRET_KEY:-minioadmin}" volumes: - - ./_data/coolify/_volumes/minio/:/data + - /data/coolify/_volumes/minio/:/data # - coolify-minio-data-dev:/data networks: - coolify diff --git a/resources/css/app.css b/resources/css/app.css index c1e8f3259..ee4ec7b77 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -53,12 +53,14 @@ a { @apply text-white; } .box { - @apply flex items-center p-2 transition-colors cursor-pointer min-h-16 bg-coolgray-200 hover:bg-coollabs-100 hover:text-white hover:no-underline min-w-[24rem]; + @apply flex p-2 transition-colors cursor-pointer min-h-16 bg-coolgray-200 hover:bg-coollabs-100 hover:text-white hover:no-underline min-w-[24rem]; } .box-without-bg { - @apply flex items-center p-2 transition-colors min-h-16 hover:text-white hover:no-underline min-w-[24rem]; + @apply flex p-2 transition-colors min-h-16 hover:text-white hover:no-underline min-w-[24rem]; +} +.description { + @apply pt-2 text-xs font-bold text-neutral-500 group-hover:text-white; } - .lds-heart { animation: lds-heart 1.2s infinite cubic-bezier(0.215, 0.61, 0.355, 1); } diff --git a/resources/js/components/MagicBar.vue b/resources/js/components/MagicBar.vue index 3ccac78b7..9797e14b6 100644 --- a/resources/js/components/MagicBar.vue +++ b/resources/js/components/MagicBar.vue @@ -1,6 +1,6 @@