diff --git a/app/Actions/Database/StartPostgresql.php b/app/Actions/Database/StartPostgresql.php index 973296380..506b2d5a4 100644 --- a/app/Actions/Database/StartPostgresql.php +++ b/app/Actions/Database/StartPostgresql.php @@ -41,6 +41,9 @@ class StartPostgresql 'networks' => [ $this->database->destination->network, ], + 'labels' => [ + 'coolify.managed' => 'true', + ], 'healthcheck' => [ 'test' => [ 'CMD-SHELL', diff --git a/app/Actions/Database/StartRedis.php b/app/Actions/Database/StartRedis.php new file mode 100644 index 000000000..6ea950f5d --- /dev/null +++ b/app/Actions/Database/StartRedis.php @@ -0,0 +1,160 @@ +database = $database; + + $startCommand = "redis-server --requirepass {$this->database->redis_password} --appendonly yes"; + + $container_name = $this->database->uuid; + $this->configuration_dir = database_configuration_dir() . '/' . $container_name; + + $this->commands = [ + "echo '####### Starting {$database->name}.'", + "mkdir -p $this->configuration_dir", + ]; + + $persistent_storages = $this->generate_local_persistent_volumes(); + $volume_names = $this->generate_local_persistent_volumes_only_volume_names(); + $environment_variables = $this->generate_environment_variables(); + $this->add_custom_redis(); + + $docker_compose = [ + 'version' => '3.8', + 'services' => [ + $container_name => [ + 'image' => $this->database->image, + 'command' => $startCommand, + 'container_name' => $container_name, + 'environment' => $environment_variables, + 'restart' => RESTART_MODE, + 'networks' => [ + $this->database->destination->network, + ], + 'labels' => [ + 'coolify.managed' => 'true', + ], + 'healthcheck' => [ + 'test' => [ + 'CMD-SHELL', + 'redis-cli', + 'ping' + ], + 'interval' => '5s', + 'timeout' => '5s', + 'retries' => 10, + 'start_period' => '5s' + ], + 'mem_limit' => $this->database->limits_memory, + 'memswap_limit' => $this->database->limits_memory_swap, + 'mem_swappiness' => $this->database->limits_memory_swappiness, + 'mem_reservation' => $this->database->limits_memory_reservation, + 'cpus' => $this->database->limits_cpus, + 'cpuset' => $this->database->limits_cpuset, + 'cpu_shares' => $this->database->limits_cpu_shares, + ] + ], + 'networks' => [ + $this->database->destination->network => [ + 'external' => true, + 'name' => $this->database->destination->network, + 'attachable' => true, + ] + ] + ]; + if (count($this->database->ports_mappings_array) > 0) { + $docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array; + } + if (count($persistent_storages) > 0) { + $docker_compose['services'][$container_name]['volumes'] = $persistent_storages; + } + if (count($volume_names) > 0) { + $docker_compose['volumes'] = $volume_names; + } + if (!is_null($this->database->redis_conf)) { + $docker_compose['services'][$container_name]['volumes'][] = [ + 'type' => 'bind', + 'source' => $this->configuration_dir . '/redis.conf', + 'target' => '/usr/local/etc/redis/redis.conf', + 'read_only' => true, + ]; + $docker_compose['services'][$container_name]['command'] = $startCommand . ' /usr/local/etc/redis/redis.conf'; + } + $docker_compose = Yaml::dump($docker_compose, 10); + $docker_compose_base64 = base64_encode($docker_compose); + $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml"; + $readme = generate_readme_file($this->database->name, now()); + $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; + $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; + $this->commands[] = "echo '####### {$database->name} started.'"; + return remote_process($this->commands, $server); + } + + private function generate_local_persistent_volumes() + { + $local_persistent_volumes = []; + foreach ($this->database->persistentStorages as $persistentStorage) { + $volume_name = $persistentStorage->host_path ?? $persistentStorage->name; + $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path; + } + return $local_persistent_volumes; + } + + private function generate_local_persistent_volumes_only_volume_names() + { + $local_persistent_volumes_names = []; + foreach ($this->database->persistentStorages as $persistentStorage) { + if ($persistentStorage->host_path) { + continue; + } + $name = $persistentStorage->name; + $local_persistent_volumes_names[$name] = [ + 'name' => $name, + 'external' => false, + ]; + } + return $local_persistent_volumes_names; + } + + private function generate_environment_variables() + { + $environment_variables = collect(); + foreach ($this->database->runtime_environment_variables as $env) { + $environment_variables->push("$env->key=$env->value"); + } + + if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('REDIS_PASSWORD'))->isEmpty()) { + $environment_variables->push("REDIS_PASSWORD={$this->database->redis_password}"); + } + + return $environment_variables->all(); + } + private function add_custom_redis() + { + if (is_null($this->database->redis_conf)) { + return; + } + $filename = 'redis.conf'; + $content = $this->database->redis_conf; + $content_base64 = base64_encode($content); + $this->commands[] = "echo '{$content_base64}' | base64 -d > $this->configuration_dir/{$filename}"; + + } +} diff --git a/app/Actions/Proxy/CheckProxy.php b/app/Actions/Proxy/CheckProxy.php new file mode 100644 index 000000000..84dc4e955 --- /dev/null +++ b/app/Actions/Proxy/CheckProxy.php @@ -0,0 +1,42 @@ +isProxyShouldRun()) { + throw new \Exception("Proxy should not run"); + } + $status = getContainerStatus($server, 'coolify-proxy'); + if ($status === 'running') { + $server->proxy->set('status', 'running'); + $server->save(); + return 'OK'; + } + $ip = $server->ip; + if ($server->id === 0) { + $ip = 'host.docker.internal'; + } + + $connection = @fsockopen($ip, '80'); + $connection = @fsockopen($ip, '443'); + $port80 = is_resource($connection) && fclose($connection); + $port443 = is_resource($connection) && fclose($connection); + ray($ip); + if ($port80) { + throw new \Exception("Port 80 is in use.
You must stop the process using this port.
Docs: https://coolify.io/docs
Discord: https://coollabs.io/discord"); + } + if ($port443) { + throw new \Exception("Port 443 is in use.
You must stop the process using this port.
Docs: https://coolify.io/docs
Discord: https://coollabs.io/discord>"); + } + } +} diff --git a/app/Actions/Proxy/StartProxy.php b/app/Actions/Proxy/StartProxy.php index d6ae7eec8..14b7fefb5 100644 --- a/app/Actions/Proxy/StartProxy.php +++ b/app/Actions/Proxy/StartProxy.php @@ -2,7 +2,6 @@ namespace App\Actions\Proxy; -use App\Enums\ProxyTypes; use App\Models\Server; use Illuminate\Support\Str; use Lorisleiva\Actions\Concerns\AsAction; @@ -11,56 +10,49 @@ use Spatie\Activitylog\Models\Activity; class StartProxy { use AsAction; - public function handle(Server $server, bool $async = true): Activity|string + public function handle(Server $server, bool $async = true): string|Activity { - $commands = collect([]); - $proxyType = $server->proxyType(); - if ($proxyType === ProxyTypes::NONE->value) { - return 'OK'; - } - $proxy_path = get_proxy_path(); - $configuration = CheckConfiguration::run($server); - if (!$configuration) { - throw new \Exception("Configuration is not synced"); - } - SaveConfiguration::run($server, $configuration); - $docker_compose_yml_base64 = base64_encode($configuration); - $server->proxy->last_applied_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value; - $server->save(); + try { + CheckProxy::run($server); - $commands = $commands->merge([ - "apt-get update > /dev/null 2>&1 || true", - "command -v lsof >/dev/null || echo '####### Installing lsof.'", - "command -v lsof >/dev/null || apt install -y lsof", - "command -v lsof >/dev/null || command -v fuser >/dev/null || apt install -y psmisc", - "mkdir -p $proxy_path && cd $proxy_path", - "echo '####### Creating Docker Compose file.'", - "echo '####### Pulling docker image.'", - 'docker compose pull', - "echo '####### Stopping existing coolify-proxy.'", - "docker compose down -v --remove-orphans > /dev/null 2>&1", - "command -v fuser >/dev/null || command -v lsof >/dev/null || echo '####### Could not kill existing processes listening on port 80 & 443. Please stop the process holding these ports...'", - "command -v lsof >/dev/null && lsof -nt -i:80 | xargs -r kill -9 || true", - "command -v lsof >/dev/null && lsof -nt -i:443 | xargs -r kill -9 || true", - "command -v fuser >/dev/null && fuser -k 80/tcp || true", - "command -v fuser >/dev/null && fuser -k 443/tcp || true", - "systemctl disable nginx > /dev/null 2>&1 || true", - "systemctl disable apache2 > /dev/null 2>&1 || true", - "systemctl disable apache > /dev/null 2>&1 || true", - "echo '####### Starting coolify-proxy.'", - 'docker compose up -d --remove-orphans', - "echo '####### Proxy installed successfully.'" - ]); - $commands = $commands->merge(connectProxyToNetworks($server)); - if ($async) { - $activity = remote_process($commands, $server); - return $activity; - } else { - instant_remote_process($commands, $server); - $server->proxy->set('status', 'running'); - $server->proxy->set('type', $proxyType); + $proxyType = $server->proxyType(); + $commands = collect([]); + $proxy_path = get_proxy_path(); + $configuration = CheckConfiguration::run($server); + if (!$configuration) { + throw new \Exception("Configuration is not synced"); + } + SaveConfiguration::run($server, $configuration); + $docker_compose_yml_base64 = base64_encode($configuration); + $server->proxy->last_applied_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value; $server->save(); - return 'OK'; + $commands = $commands->merge([ + "mkdir -p $proxy_path && cd $proxy_path", + "echo 'Creating required Docker Compose file.'", + "echo 'Pulling docker image.'", + 'docker compose pull', + "echo 'Stopping existing coolify-proxy.'", + "docker compose down -v --remove-orphans > /dev/null 2>&1", + "echo 'Starting coolify-proxy.'", + 'docker compose up -d --remove-orphans', + "echo 'Proxy started successfully.'" + ]); + $commands = $commands->merge(connectProxyToNetworks($server)); + if ($async) { + $activity = remote_process($commands, $server); + return $activity; + } else { + instant_remote_process($commands, $server); + $server->proxy->set('status', 'running'); + $server->proxy->set('type', $proxyType); + $server->save(); + return 'OK'; + } + } catch(\Throwable $e) { + ray($e); + throw $e; } + + } } diff --git a/app/Console/Commands/ResourcesDelete.php b/app/Console/Commands/ResourcesDelete.php index 3b0134f47..1c90f8e6b 100644 --- a/app/Console/Commands/ResourcesDelete.php +++ b/app/Console/Commands/ResourcesDelete.php @@ -43,23 +43,24 @@ class ResourcesDelete extends Command $this->deleteDatabase(); } elseif ($resource === 'Service') { $this->deleteService(); - } elseif($resource === 'Server') { + } elseif ($resource === 'Server') { $this->deleteServer(); } } - private function deleteServer() { + private function deleteServer() + { $servers = Server::all(); if ($servers->count() === 0) { $this->error('There are no applications to delete.'); return; } $serversToDelete = multiselect( - 'What server do you want to delete?', - $servers->pluck('id')->sort()->toArray(), + label: 'What server do you want to delete?', + options: $servers->pluck('name', 'id')->sortKeys(), ); - foreach ($serversToDelete as $id) { - $toDelete = Server::find($id); + foreach ($serversToDelete as $server) { + $toDelete = $servers->where('id', $server)->first(); $this->info($toDelete); $confirmed = confirm("Are you sure you want to delete all selected resources?"); if (!$confirmed) { @@ -77,11 +78,12 @@ class ResourcesDelete extends Command } $applicationsToDelete = multiselect( 'What application do you want to delete?', - $applications->pluck('name')->sort()->toArray(), + $applications->pluck('name', 'id')->sortKeys(), ); foreach ($applicationsToDelete as $application) { - $toDelete = $applications->where('name', $application)->first(); + ray($application); + $toDelete = $applications->where('id', $application)->first(); $this->info($toDelete); $confirmed = confirm("Are you sure you want to delete all selected resources? "); if (!$confirmed) { @@ -99,11 +101,11 @@ class ResourcesDelete extends Command } $databasesToDelete = multiselect( 'What database do you want to delete?', - $databases->pluck('name')->sort()->toArray(), + $databases->pluck('name', 'id')->sortKeys(), ); foreach ($databasesToDelete as $database) { - $toDelete = $databases->where('name', $database)->first(); + $toDelete = $databases->where('id', $database)->first(); $this->info($toDelete); $confirmed = confirm("Are you sure you want to delete all selected resources?"); if (!$confirmed) { @@ -111,7 +113,6 @@ class ResourcesDelete extends Command } $toDelete->delete(); } - } private function deleteService() { @@ -122,11 +123,11 @@ class ResourcesDelete extends Command } $servicesToDelete = multiselect( 'What service do you want to delete?', - $services->pluck('name')->sort()->toArray(), + $services->pluck('name', 'id')->sortKeys(), ); foreach ($servicesToDelete as $service) { - $toDelete = $services->where('name', $service)->first(); + $toDelete = $services->where('id', $service)->first(); $this->info($toDelete); $confirmed = confirm("Are you sure you want to delete all selected resources?"); if (!$confirmed) { diff --git a/app/Http/Controllers/DatabaseController.php b/app/Http/Controllers/DatabaseController.php index 958d6d5ab..684e427f9 100644 --- a/app/Http/Controllers/DatabaseController.php +++ b/app/Http/Controllers/DatabaseController.php @@ -19,7 +19,7 @@ class DatabaseController extends Controller if (!$environment) { return redirect()->route('dashboard'); } - $database = $environment->databases->where('uuid', request()->route('database_uuid'))->first(); + $database = $environment->databases()->where('uuid', request()->route('database_uuid'))->first(); if (!$database) { return redirect()->route('dashboard'); } @@ -37,7 +37,7 @@ class DatabaseController extends Controller if (!$environment) { return redirect()->route('dashboard'); } - $database = $environment->databases->where('uuid', request()->route('database_uuid'))->first(); + $database = $environment->databases()->where('uuid', request()->route('database_uuid'))->first(); if (!$database) { return redirect()->route('dashboard'); } @@ -64,10 +64,18 @@ class DatabaseController extends Controller if (!$environment) { return redirect()->route('dashboard'); } - $database = $environment->databases->where('uuid', request()->route('database_uuid'))->first(); + $database = $environment->databases()->where('uuid', request()->route('database_uuid'))->first(); if (!$database) { return redirect()->route('dashboard'); } + // No backups for redis + if ($database->getMorphClass() === 'App\Models\StandaloneRedis') { + return redirect()->route('project.database.configuration', [ + 'project_uuid' => $project->uuid, + 'environment_name' => $environment->name, + 'database_uuid' => $database->uuid, + ]); + } return view('project.database.backups.all', [ 'database' => $database, 's3s' => currentTeam()->s3s, diff --git a/app/Http/Controllers/ProjectController.php b/app/Http/Controllers/ProjectController.php index 7b8cfbfd1..7e67a588c 100644 --- a/app/Http/Controllers/ProjectController.php +++ b/app/Http/Controllers/ProjectController.php @@ -59,11 +59,15 @@ class ProjectController extends Controller return redirect()->route('dashboard'); } if (in_array($type, DATABASE_TYPES)) { - $standalone_postgresql = create_standalone_postgresql($environment->id, $destination_uuid); + if ($type->value() === "postgresql") { + $database = create_standalone_postgresql($environment->id, $destination_uuid); + } else if ($type->value() === 'redis') { + $database = create_standalone_redis($environment->id, $destination_uuid); + } return redirect()->route('project.database.configuration', [ 'project_uuid' => $project->uuid, 'environment_name' => $environment->name, - 'database_uuid' => $standalone_postgresql->uuid, + 'database_uuid' => $database->uuid, ]); } if ($type->startsWith('one-click-service-') && !is_null( (int)$server_id)) { diff --git a/app/Http/Livewire/Project/Database/BackupEdit.php b/app/Http/Livewire/Project/Database/BackupEdit.php index ea217f526..951f95468 100644 --- a/app/Http/Livewire/Project/Database/BackupEdit.php +++ b/app/Http/Livewire/Project/Database/BackupEdit.php @@ -17,6 +17,7 @@ class BackupEdit extends Component 'backup.number_of_backups_locally' => 'required|integer|min:1', 'backup.save_s3' => 'required|boolean', 'backup.s3_storage_id' => 'nullable|integer', + 'backup.databases_to_backup' => 'nullable', ]; protected $validationAttributes = [ 'backup.enabled' => 'Enabled', @@ -24,6 +25,7 @@ class BackupEdit extends Component 'backup.number_of_backups_locally' => 'Number of Backups Locally', 'backup.save_s3' => 'Save to S3', 'backup.s3_storage_id' => 'S3 Storage', + 'backup.databases_to_backup' => 'Databases to Backup', ]; protected $messages = [ 'backup.s3_storage_id' => 'Select a S3 Storage', @@ -37,7 +39,6 @@ class BackupEdit extends Component } } - public function delete() { // TODO: Delete backup from server and add a confirmation modal @@ -49,6 +50,7 @@ class BackupEdit extends Component { try { $this->custom_validate(); + $this->backup->save(); $this->backup->refresh(); $this->emit('success', 'Backup updated successfully'); @@ -71,9 +73,11 @@ class BackupEdit extends Component public function submit() { - ray($this->backup->s3_storage_id); try { $this->custom_validate(); + if ($this->backup->databases_to_backup == '' || $this->backup->databases_to_backup === null) { + $this->backup->databases_to_backup = null; + } $this->backup->save(); $this->backup->refresh(); $this->emit('success', 'Backup updated successfully'); diff --git a/app/Http/Livewire/Project/Database/BackupNow.php b/app/Http/Livewire/Project/Database/BackupNow.php index ea25c4744..35bd8318d 100644 --- a/app/Http/Livewire/Project/Database/BackupNow.php +++ b/app/Http/Livewire/Project/Database/BackupNow.php @@ -13,6 +13,6 @@ class BackupNow extends Component dispatch(new DatabaseBackupJob( backup: $this->backup )); - $this->emit('success', 'Backup queued. It will be available in a few minutes'); + $this->emit('success', 'Backup queued. It will be available in a few minutes.'); } } diff --git a/app/Http/Livewire/Project/Database/CreateScheduledBackup.php b/app/Http/Livewire/Project/Database/CreateScheduledBackup.php index a364938c0..35ecebdd3 100644 --- a/app/Http/Livewire/Project/Database/CreateScheduledBackup.php +++ b/app/Http/Livewire/Project/Database/CreateScheduledBackup.php @@ -32,7 +32,7 @@ class CreateScheduledBackup extends Component $this->emit('error', 'Invalid Cron / Human expression.'); return; } - ScheduledDatabaseBackup::create([ + $payload = [ 'enabled' => true, 'frequency' => $this->frequency, 'save_s3' => $this->save_s3, @@ -40,7 +40,11 @@ class CreateScheduledBackup extends Component 'database_id' => $this->database->id, 'database_type' => $this->database->getMorphClass(), 'team_id' => currentTeam()->id, - ]); + ]; + if ($this->database->type() === 'standalone-postgresql') { + $payload['databases_to_backup'] = $this->database->postgres_db; + } + ScheduledDatabaseBackup::create($payload); $this->emit('refreshScheduledBackups'); } catch (\Throwable $e) { handleError($e, $this); diff --git a/app/Http/Livewire/Project/Database/Heading.php b/app/Http/Livewire/Project/Database/Heading.php index 3389ac80a..2c739f087 100644 --- a/app/Http/Livewire/Project/Database/Heading.php +++ b/app/Http/Livewire/Project/Database/Heading.php @@ -3,6 +3,7 @@ namespace App\Http\Livewire\Project\Database; use App\Actions\Database\StartPostgresql; +use App\Actions\Database\StartRedis; use App\Jobs\ContainerStatusJob; use Livewire\Component; @@ -26,6 +27,7 @@ class Heading extends Component { dispatch_sync(new ContainerStatusJob($this->database->destination->server)); $this->database->refresh(); + $this->emit('refresh'); } public function mount() @@ -40,7 +42,7 @@ class Heading extends Component $this->database->destination->server ); if ($this->database->is_public) { - stopPostgresProxy($this->database); + stopDatabaseProxy($this->database); $this->database->is_public = false; } $this->database->status = 'exited'; @@ -55,5 +57,9 @@ class Heading extends Component $activity = resolve(StartPostgresql::class)($this->database->destination->server, $this->database); $this->emit('newMonitorActivity', $activity->id); } + if ($this->database->type() === 'standalone-redis') { + $activity = StartRedis::run($this->database->destination->server, $this->database); + $this->emit('newMonitorActivity', $activity->id); + } } } diff --git a/app/Http/Livewire/Project/Database/Postgresql/General.php b/app/Http/Livewire/Project/Database/Postgresql/General.php index 5a03908e1..bbd1de0ff 100644 --- a/app/Http/Livewire/Project/Database/Postgresql/General.php +++ b/app/Http/Livewire/Project/Database/Postgresql/General.php @@ -67,10 +67,10 @@ class General extends Component } if ($this->database->is_public) { $this->emit('success', 'Starting TCP proxy...'); - startPostgresProxy($this->database); + startDatabaseProxy($this->database); $this->emit('success', 'Database is now publicly accessible.'); } else { - stopPostgresProxy($this->database); + stopDatabaseProxy($this->database); $this->emit('success', 'Database is no longer publicly accessible.'); } $this->getDbUrl(); diff --git a/app/Http/Livewire/Project/Database/Redis/General.php b/app/Http/Livewire/Project/Database/Redis/General.php new file mode 100644 index 000000000..c8e15062a --- /dev/null +++ b/app/Http/Livewire/Project/Database/Redis/General.php @@ -0,0 +1,92 @@ + 'required', + 'database.description' => 'nullable', + 'database.redis_conf' => 'nullable', + 'database.redis_password' => 'required', + 'database.image' => 'required', + 'database.ports_mappings' => 'nullable', + 'database.is_public' => 'nullable|boolean', + 'database.public_port' => 'nullable|integer', + ]; + protected $validationAttributes = [ + 'database.name' => 'Name', + 'database.description' => 'Description', + 'database.redis_conf' => 'Redis Configuration', + 'database.redis_password' => 'Redis Password', + 'database.image' => 'Image', + 'database.ports_mappings' => 'Port Mapping', + 'database.is_public' => 'Is Public', + 'database.public_port' => 'Public Port', + ]; + public function submit() { + try { + $this->validate(); + if ($this->database->redis_conf === "") { + $this->database->redis_conf = null; + } + $this->database->save(); + $this->emit('success', 'Database updated successfully.'); + } catch (Exception $e) { + return handleError($e, $this); + } + } + public function instantSave() + { + try { + if ($this->database->is_public && !$this->database->public_port) { + $this->emit('error', 'Public port is required.'); + $this->database->is_public = false; + return; + } + if ($this->database->is_public) { + $this->emit('success', 'Starting TCP proxy...'); + startDatabaseProxy($this->database); + $this->emit('success', 'Database is now publicly accessible.'); + } else { + stopDatabaseProxy($this->database); + $this->emit('success', 'Database is no longer publicly accessible.'); + } + $this->getDbUrl(); + $this->database->save(); + } catch(\Throwable $e) { + $this->database->is_public = !$this->database->is_public; + return handleError($e, $this); + } + } + public function refresh(): void + { + $this->database->refresh(); + } + + public function mount() + { + $this->getDbUrl(); + } + public function getDbUrl() { + + if ($this->database->is_public) { + $this->db_url = "redis://{$this->database->redis_password}@{$this->database->destination->server->getIp}:{$this->database->public_port}/0"; + } else { + $this->db_url = "redis://{$this->database->redis_password}@{$this->database->uuid}:5432/0"; + } + } + public function render() + { + return view('livewire.project.database.redis.general'); + } +} diff --git a/app/Http/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Http/Livewire/Project/Shared/EnvironmentVariable/All.php index a95c94977..ce7c5ae40 100644 --- a/app/Http/Livewire/Project/Shared/EnvironmentVariable/All.php +++ b/app/Http/Livewire/Project/Shared/EnvironmentVariable/All.php @@ -75,6 +75,9 @@ class All extends Component case 'standalone-postgresql': $environment->standalone_postgresql_id = $this->resource->id; break; + case 'standalone-redis': + $environment->standalone_redis_id = $this->resource->id; + break; case 'service': $environment->service_id = $this->resource->id; break; diff --git a/app/Http/Livewire/Project/Shared/Logs.php b/app/Http/Livewire/Project/Shared/Logs.php index 69848a7c5..15a4e510c 100644 --- a/app/Http/Livewire/Project/Shared/Logs.php +++ b/app/Http/Livewire/Project/Shared/Logs.php @@ -6,12 +6,13 @@ use App\Models\Application; use App\Models\Server; use App\Models\Service; use App\Models\StandalonePostgresql; +use App\Models\StandaloneRedis; use Livewire\Component; class Logs extends Component { public ?string $type = null; - public Application|StandalonePostgresql|Service $resource; + public Application|StandalonePostgresql|Service|StandaloneRedis $resource; public Server $server; public ?string $container = null; public $parameters; @@ -33,7 +34,14 @@ class Logs extends Component } } else if (data_get($this->parameters, 'database_uuid')) { $this->type = 'database'; - $this->resource = StandalonePostgresql::where('uuid', $this->parameters['database_uuid'])->firstOrFail(); + $resource = StandalonePostgresql::where('uuid', $this->parameters['database_uuid'])->first(); + if (is_null($resource)) { + $resource = StandaloneRedis::where('uuid', $this->parameters['database_uuid'])->first(); + if (is_null($resource)) { + abort(404); + } + } + $this->resource = $resource; $this->status = $this->resource->status; $this->server = $this->resource->destination->server; $this->container = $this->resource->uuid; diff --git a/app/Http/Livewire/Server/Form.php b/app/Http/Livewire/Server/Form.php index f773155d5..816514aeb 100644 --- a/app/Http/Livewire/Server/Form.php +++ b/app/Http/Livewire/Server/Form.php @@ -21,7 +21,7 @@ class Form extends Component protected $rules = [ 'server.name' => 'required|min:6', 'server.description' => 'nullable', - 'server.ip' => 'required|ip', + 'server.ip' => 'required', 'server.user' => 'required', 'server.port' => 'required', 'server.settings.is_cloudflare_tunnel' => 'required', @@ -45,7 +45,8 @@ class Form extends Component $this->wildcard_domain = $this->server->settings->wildcard_domain; $this->cleanup_after_percentage = $this->server->settings->cleanup_after_percentage; } - public function serverRefresh() { + public function serverRefresh() + { $this->validateServer(); } public function instantSave() @@ -61,7 +62,8 @@ class Form extends Component $activity = InstallDocker::run($this->server); $this->emit('newMonitorActivity', $activity->id); } - public function checkLocalhostConnection() { + public function checkLocalhostConnection() + { $uptime = $this->server->validateConnection(); if ($uptime) { $this->emit('success', 'Server is reachable.'); @@ -80,7 +82,7 @@ class Form extends Component if ($uptime) { $install && $this->emit('success', 'Server is reachable.'); } else { - $install &&$this->emit('error', 'Server is not reachable. Please check your connection and configuration.'); + $install && $this->emit('error', 'Server is not reachable. Please check your connection and configuration.'); return; } $dockerInstalled = $this->server->validateDockerEngine(); @@ -120,7 +122,14 @@ class Form extends Component } public function submit() { - $this->validate(); + if(isCloud() && !isDev()) { + $this->validate(); + $this->validate([ + 'server.ip' => 'required|ip', + ]); + } else { + $this->validate(); + } $uniqueIPs = Server::all()->reject(function (Server $server) { return $server->id === $this->server->id; })->pluck('ip')->toArray(); diff --git a/app/Http/Livewire/Server/Proxy/Deploy.php b/app/Http/Livewire/Server/Proxy/Deploy.php index ad5884e18..39995e077 100644 --- a/app/Http/Livewire/Server/Proxy/Deploy.php +++ b/app/Http/Livewire/Server/Proxy/Deploy.php @@ -2,6 +2,7 @@ namespace App\Http\Livewire\Server\Proxy; +use App\Actions\Proxy\CheckProxy; use App\Actions\Proxy\StartProxy; use App\Models\Server; use Livewire\Component; @@ -11,18 +12,40 @@ class Deploy extends Component public Server $server; public bool $traefikDashboardAvailable = false; public ?string $currentRoute = null; - protected $listeners = ['proxyStatusUpdated', 'traefikDashboardAvailable', 'serverRefresh' => 'proxyStatusUpdated']; + public ?string $serverIp = null; - public function mount() { + protected $listeners = ['proxyStatusUpdated', 'traefikDashboardAvailable', 'serverRefresh' => 'proxyStatusUpdated', "checkProxy", "startProxy"]; + + public function mount() + { + if ($this->server->id === 0) { + $this->serverIp = base_ip(); + } else { + $this->serverIp = $this->server->ip; + } $this->currentRoute = request()->route()->getName(); } - public function traefikDashboardAvailable(bool $data) { + public function traefikDashboardAvailable(bool $data) + { $this->traefikDashboardAvailable = $data; } public function proxyStatusUpdated() { $this->server->refresh(); } + public function ip() + { + } + public function checkProxy() + { + try { + CheckProxy::run($this->server); + $this->emit('startProxyPolling'); + $this->emit('proxyChecked'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } public function startProxy() { try { diff --git a/app/Http/Livewire/Server/Proxy/Modal.php b/app/Http/Livewire/Server/Proxy/Modal.php index ef9948910..2674abe3d 100644 --- a/app/Http/Livewire/Server/Proxy/Modal.php +++ b/app/Http/Livewire/Server/Proxy/Modal.php @@ -11,8 +11,6 @@ class Modal extends Component public function proxyStatusUpdated() { - $this->server->proxy->set('status', 'running'); - $this->server->save(); $this->emit('proxyStatusUpdated'); } } diff --git a/app/Http/Livewire/Server/Proxy/Status.php b/app/Http/Livewire/Server/Proxy/Status.php index 5cfd22082..90deb1455 100644 --- a/app/Http/Livewire/Server/Proxy/Status.php +++ b/app/Http/Livewire/Server/Proxy/Status.php @@ -2,6 +2,7 @@ namespace App\Http\Livewire\Server\Proxy; +use App\Actions\Proxy\CheckProxy; use App\Jobs\ContainerStatusJob; use App\Models\Server; use Livewire\Component; @@ -9,12 +10,42 @@ use Livewire\Component; class Status extends Component { public Server $server; + public bool $polling = false; + public int $numberOfPolls = 0; - protected $listeners = ['proxyStatusUpdated']; + protected $listeners = ['proxyStatusUpdated', 'startProxyPolling']; + public function startProxyPolling() + { + $this->polling = true; + } public function proxyStatusUpdated() { $this->server->refresh(); } + public function checkProxy(bool $notification = false) + { + try { + if ($this->polling) { + if ($this->numberOfPolls >= 10) { + $this->polling = false; + $this->numberOfPolls = 0; + $notification && $this->emit('error', 'Proxy is not running.'); + return; + } + $this->numberOfPolls++; + } + CheckProxy::run($this->server); + $this->emit('proxyStatusUpdated'); + if ($this->server->proxy->status === 'running') { + $this->polling = false; + $notification && $this->emit('success', 'Proxy is running.'); + } else { + $notification && $this->emit('error', 'Proxy is not running.'); + } + } catch (\Throwable $e) { + return handleError($e, $this); + } + } public function getProxyStatus() { try { @@ -24,11 +55,4 @@ class Status extends Component return handleError($e, $this); } } - public function getProxyStatusWithNoti() - { - if ($this->server->isFunctional()) { - $this->emit('success', 'Refreshed proxy status.'); - $this->getProxyStatus(); - } - } } diff --git a/app/Http/Livewire/Settings/Backup.php b/app/Http/Livewire/Settings/Backup.php index 263db027d..916344ddc 100644 --- a/app/Http/Livewire/Settings/Backup.php +++ b/app/Http/Livewire/Settings/Backup.php @@ -78,10 +78,10 @@ class Backup extends Component dispatch(new DatabaseBackupJob( backup: $this->backup )); - $this->emit('success', 'Backup queued. It will be available in a few minutes'); + $this->emit('success', 'Backup queued. It will be available in a few minutes.'); } public function submit() { - $this->emit('success', 'Backup updated successfully'); + $this->emit('success', 'Backup updated successfully.'); } } diff --git a/app/Jobs/ContainerStatusJob.php b/app/Jobs/ContainerStatusJob.php index 9b4ef5699..b68a7c89b 100644 --- a/app/Jobs/ContainerStatusJob.php +++ b/app/Jobs/ContainerStatusJob.php @@ -27,11 +27,6 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted public $tries = 1; public $timeout = 120; - public function __construct(public Server $server) - { - - } - public function middleware(): array { return [(new WithoutOverlapping($this->server->uuid))->dontRelease()]; @@ -41,6 +36,14 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted { return $this->server->uuid; } + + public function __construct(public Server $server) + { + if (isDev()) { + $this->handle(); + } + } + public function handle() { try { diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index cc65cea9b..742ed9cee 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -66,50 +66,77 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted ray('database not running'); return; } + $databaseType = $this->database->type(); + $databasesToBackup = data_get($this->backup, 'databases_to_backup'); + + if (is_null($databasesToBackup)) { + if ($databaseType === 'standalone-postgresql') { + $databasesToBackup = [$this->database->postgres_db]; + } else { + return; + } + } else { + $databasesToBackup = explode(',', $databasesToBackup); + $databasesToBackup = array_map('trim', $databasesToBackup); + } $this->container_name = $this->database->uuid; $this->backup_dir = backup_dir() . "/databases/" . Str::of($this->team->name)->slug() . '-' . $this->team->id . '/' . $this->container_name; if ($this->database->name === 'coolify-db') { + $databasesToBackup = ['coolify']; $this->container_name = "coolify-db"; $ip = Str::slug($this->server->ip); $this->backup_dir = backup_dir() . "/coolify" . "/coolify-db-$ip"; } - $this->backup_file = "/pg-backup-customformat-" . Carbon::now()->timestamp . ".backup"; - $this->backup_location = $this->backup_dir . $this->backup_file; - - $this->backup_log = ScheduledDatabaseBackupExecution::create([ - 'filename' => $this->backup_location, - 'scheduled_database_backup_id' => $this->backup->id, - ]); - if ($this->database->type() === 'standalone-postgresql') { - $this->backup_standalone_postgresql(); + foreach ($databasesToBackup as $database) { + $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_standalone_postgresql($database); + } + $size = $this->calculate_size(); + $this->remove_old_backups(); + if ($this->backup->save_s3) { + $this->upload_to_s3(); + } + $this->team->notify(new BackupSuccess($this->backup, $this->database)); + $this->backup_log->update([ + 'status' => 'success', + 'message' => $this->backup_output, + 'size' => $size, + ]); + } catch (\Throwable $e) { + $this->backup_log->update([ + 'status' => 'failed', + 'message' => $this->backup_output, + 'size' => $size, + 'filename' => null + ]); + send_internal_notification('DatabaseBackupJob failed with: ' . $e->getMessage()); + $this->team->notify(new BackupFailed($this->backup, $this->database, $this->backup_output)); + throw $e; + } } - $this->calculate_size(); - $this->remove_old_backups(); - if ($this->backup->save_s3) { - $this->upload_to_s3(); - } - $this->save_backup_logs(); - $this->team->notify(new BackupSuccess($this->backup, $this->database)); - $this->backup_status = 'success'; } catch (\Throwable $e) { - $this->backup_status = 'failed'; send_internal_notification('DatabaseBackupJob failed with: ' . $e->getMessage()); - $this->team->notify(new BackupFailed($this->backup, $this->database, $this->backup_output)); throw $e; - } finally { - $this->backup_log->update([ - 'status' => $this->backup_status, - ]); } } - private function backup_standalone_postgresql(): void + private function backup_standalone_postgresql(string $database): void { try { ray($this->backup_dir); $commands[] = "mkdir -p " . $this->backup_dir; - $commands[] = "docker exec $this->container_name pg_dump -Fc -U {$this->database->postgres_user} > $this->backup_location"; + $commands[] = "docker exec $this->container_name pg_dump --format=custom --no-acl --no-owner --username {$this->database->postgres_user} $database > $this->backup_location"; $this->backup_output = instant_remote_process($commands, $this->server); $this->backup_output = trim($this->backup_output); if ($this->backup_output === '') { @@ -119,6 +146,7 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted } 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; } } @@ -131,9 +159,9 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted } } - private function calculate_size(): void + private function calculate_size() { - $this->size = instant_remote_process(["du -b $this->backup_location | cut -f1"], $this->server); + return instant_remote_process(["du -b $this->backup_location | cut -f1"], $this->server, false); } private function remove_old_backups(): void @@ -180,13 +208,4 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted instant_remote_process([$command], $this->server); } } - - private function save_backup_logs(): void - { - $this->backup_log->update([ - 'status' => $this->backup_status, - 'message' => $this->backup_output, - 'size' => $this->size, - ]); - } } diff --git a/app/Jobs/DockerCleanupJob.php b/app/Jobs/DockerCleanupJob.php index 08cb16959..411a0f464 100644 --- a/app/Jobs/DockerCleanupJob.php +++ b/app/Jobs/DockerCleanupJob.php @@ -47,20 +47,7 @@ class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted if (!$this->server->isFunctional()) { return; } - if (isDev()) { - $this->dockerRootFilesystem = "/"; - } else { - $this->dockerRootFilesystem = instant_remote_process( - [ - "stat --printf=%m $(docker info --format '{{json .DockerRootDir}}'' |sed 's/\"//g')" - ], - $this->server, - false - ); - } - if (!$this->dockerRootFilesystem) { - return; - } + $this->dockerRootFilesystem = "/"; $this->usageBefore = $this->getFilesystemUsage(); if ($this->usageBefore >= $this->server->settings->cleanup_after_percentage) { ray('Cleaning up ' . $this->server->name)->color('orange'); diff --git a/app/Models/Environment.php b/app/Models/Environment.php index 624787ba6..f66bf48f2 100644 --- a/app/Models/Environment.php +++ b/app/Models/Environment.php @@ -14,7 +14,7 @@ class Environment extends Model public function can_delete_environment() { - return $this->applications()->count() == 0 && $this->postgresqls()->count() == 0 && $this->services()->count() == 0; + return $this->applications()->count() == 0 && $this->redis()->count() == 0 && $this->postgresqls()->count() == 0 && $this->services()->count() == 0; } public function applications() @@ -26,10 +26,16 @@ class Environment extends Model { return $this->hasMany(StandalonePostgresql::class); } + public function redis() + { + return $this->hasMany(StandaloneRedis::class); + } public function databases() { - return $this->postgresqls(); + $postgresqls = $this->postgresqls; + $redis = $this->redis; + return $postgresqls->concat($redis); } public function project() diff --git a/app/Models/Project.php b/app/Models/Project.php index 13f86bf66..f8f9622b8 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -52,4 +52,8 @@ class Project extends BaseModel { return $this->hasManyThrough(StandalonePostgresql::class, Environment::class); } + public function redis() + { + return $this->hasManyThrough(StandaloneRedis::class, Environment::class); + } } diff --git a/app/Models/Server.php b/app/Models/Server.php index 313040afa..fa8fdbe39 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -123,7 +123,8 @@ class Server extends BaseModel { return $this->destinations()->map(function ($standaloneDocker) { $postgresqls = $standaloneDocker->postgresqls; - return $postgresqls?->concat([]) ?? collect([]); + $redis = $standaloneDocker->redis; + return $postgresqls->concat($redis); })->flatten(); } public function applications() diff --git a/app/Models/StandaloneDocker.php b/app/Models/StandaloneDocker.php index 16d85d956..a594b854a 100644 --- a/app/Models/StandaloneDocker.php +++ b/app/Models/StandaloneDocker.php @@ -15,6 +15,10 @@ class StandaloneDocker extends BaseModel { return $this->morphMany(StandalonePostgresql::class, 'destination'); } + public function redis() + { + return $this->morphMany(StandaloneRedis::class, 'destination'); + } public function server() { diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php new file mode 100644 index 000000000..f48dc7bb6 --- /dev/null +++ b/app/Models/StandaloneRedis.php @@ -0,0 +1,103 @@ + 'redis-data-' . $database->uuid, + 'mount_path' => '/data', + 'host_path' => null, + 'resource_id' => $database->id, + 'resource_type' => $database->getMorphClass(), + 'is_readonly' => true + ]); + }); + static::deleting(function ($database) { + // Stop Container + instant_remote_process( + ["docker rm -f {$database->uuid}"], + $database->destination->server, + false + ); + // Stop TCP Proxy + if ($database->is_public) { + instant_remote_process(["docker rm -f {$database->uuid}-proxy"], $database->destination->server, false); + } + $database->scheduledBackups()->delete(); + $database->persistentStorages()->delete(); + $database->environment_variables()->delete(); + // Remove Volume + instant_remote_process(['docker volume rm postgres-data-' . $database->uuid], $database->destination->server, false); + }); + } + + public function portsMappings(): Attribute + { + return Attribute::make( + set: fn ($value) => $value === "" ? null : $value, + ); + } + + // Normal Deployments + + public function portsMappingsArray(): Attribute + { + return Attribute::make( + get: fn () => is_null($this->ports_mappings) + ? [] + : explode(',', $this->ports_mappings), + + ); + } + + public function type(): string + { + return 'standalone-redis'; + } + + public function environment() + { + return $this->belongsTo(Environment::class); + } + + public function fileStorages() + { + return $this->morphMany(LocalFileVolume::class, 'resource'); + } + + public function destination() + { + return $this->morphTo(); + } + + public function environment_variables(): HasMany + { + return $this->hasMany(EnvironmentVariable::class); + } + + public function runtime_environment_variables(): HasMany + { + return $this->hasMany(EnvironmentVariable::class); + } + + public function persistentStorages() + { + return $this->morphMany(LocalPersistentVolume::class, 'resource'); + } + + public function scheduledBackups() + { + return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); + } +} diff --git a/bootstrap/helpers/constants.php b/bootstrap/helpers/constants.php index c6fbc3c4d..f915ea041 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 3b8e893c7..3c4f0dfd9 100644 --- a/bootstrap/helpers/databases.php +++ b/bootstrap/helpers/databases.php @@ -3,6 +3,7 @@ use App\Models\Server; use App\Models\StandaloneDocker; use App\Models\StandalonePostgresql; +use App\Models\StandaloneRedis; use Visus\Cuid2\Cuid2; function generate_database_name(string $type): string @@ -27,6 +28,21 @@ function create_standalone_postgresql($environment_id, $destination_uuid): Stand ]); } +function create_standalone_redis($environment_id, $destination_uuid): StandaloneRedis +{ + $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); + if (!$destination) { + throw new Exception('Destination not found'); + } + return StandaloneRedis::create([ + 'name' => generate_database_name('redis'), + 'redis_password' => \Illuminate\Support\Str::password(symbols: false), + 'environment_id' => $environment_id, + 'destination_id' => $destination->id, + 'destination_type' => $destination->getMorphClass(), + ]); +} + /** * Delete file locally on the filesystem. * @param string $filename diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php index abada1bda..08f134d15 100644 --- a/bootstrap/helpers/proxy.php +++ b/bootstrap/helpers/proxy.php @@ -3,6 +3,7 @@ use App\Actions\Proxy\SaveConfiguration; use App\Models\Server; use App\Models\StandalonePostgresql; +use App\Models\StandaloneRedis; use Symfony\Component\Yaml\Yaml; function get_proxy_path() @@ -21,7 +22,7 @@ function connectProxyToNetworks(Server $server) } $commands = $networks->map(function ($network) { return [ - "echo '####### Connecting coolify-proxy to $network network...'", + "echo 'Connecting coolify-proxy to $network network...'", "docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null || docker network create --attachable $network >/dev/null", "docker network connect $network coolify-proxy >/dev/null 2>&1 || true", ]; @@ -187,8 +188,14 @@ function setup_default_redirect_404(string|null $redirect_url, Server $server) } } -function startPostgresProxy(StandalonePostgresql $database) +function startDatabaseProxy(StandalonePostgresql|StandaloneRedis $database) { + $internalPort = null; + if ($database->getMorphClass()=== 'App\Models\StandaloneRedis') { + $internalPort = 6379; + } else if ($database->getMorphClass()=== 'App\Models\StandalonePostgresql') { + $internalPort = 5432; + } $containerName = "{$database->uuid}-proxy"; $configuration_dir = database_proxy_dir($database->uuid); $nginxconf = <<public_port; - proxy_pass $database->uuid:5432; + proxy_pass $database->uuid:$internalPort; } } EOF; @@ -260,7 +267,7 @@ EOF; "docker compose --project-directory {$configuration_dir} up --build -d >/dev/null", ], $database->destination->server); } -function stopPostgresProxy(StandalonePostgresql $database) +function stopDatabaseProxy(StandalonePostgresql|StandaloneRedis $database) { instant_remote_process(["docker rm -f {$database->uuid}-proxy"], $database->destination->server); } diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php index b32db5b2c..25a6d2db7 100644 --- a/bootstrap/helpers/remoteProcess.php +++ b/bootstrap/helpers/remoteProcess.php @@ -108,12 +108,13 @@ function generateSshCommand(Server $server, string $command, bool $isMux = true) } function instant_remote_process(Collection|array $command, Server $server, $throwError = true) { + $timeout = config('constants.ssh.command_timeout'); if ($command instanceof Collection) { $command = $command->toArray(); } $command_string = implode("\n", $command); $ssh_command = generateSshCommand($server, $command_string); - $process = Process::run($ssh_command); + $process = Process::timeout($timeout)->run($ssh_command); $output = trim($process->output()); $exitCode = $process->exitCode(); if ($exitCode !== 0) { diff --git a/config/sentry.php b/config/sentry.php index b1e4266f8..7a6e8e5d2 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.81', + 'release' => '4.0.0-beta.82', // 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 a85ede531..7e5447038 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('redis_password'); + $table->longText('redis_conf')->nullable(); + + $table->string('status')->default('exited'); + + $table->string('image')->default('redis:7.2'); + + $table->boolean('is_public')->default(false); + $table->integer('public_port')->nullable(); + $table->text('ports_mappings')->nullable(); + + $table->string('limits_memory')->default("0"); + $table->string('limits_memory_swap')->default("0"); + $table->integer('limits_memory_swappiness')->default(60); + $table->string('limits_memory_reservation')->default("0"); + + $table->string('limits_cpus')->default("0"); + $table->string('limits_cpuset')->nullable()->default("0"); + $table->integer('limits_cpu_shares')->default(1024); + + $table->timestamp('started_at')->nullable(); + $table->morphs('destination'); + $table->foreignId('environment_id')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('standalone_redis'); + } +}; diff --git a/database/migrations/2023_10_12_132431_add_standalone_redis_to_environment_variables_table.php b/database/migrations/2023_10_12_132431_add_standalone_redis_to_environment_variables_table.php new file mode 100644 index 000000000..2fdacc9e5 --- /dev/null +++ b/database/migrations/2023_10_12_132431_add_standalone_redis_to_environment_variables_table.php @@ -0,0 +1,28 @@ +foreignId('standalone_redis_id')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('environment_variables', function (Blueprint $table) { + $table->dropColumn('standalone_redis_id'); + }); + } +}; diff --git a/database/migrations/2023_10_12_132432_add_database_selection_to_backups.php b/database/migrations/2023_10_12_132432_add_database_selection_to_backups.php new file mode 100644 index 000000000..40ade57e8 --- /dev/null +++ b/database/migrations/2023_10_12_132432_add_database_selection_to_backups.php @@ -0,0 +1,34 @@ +text('databases_to_backup')->nullable(); + }); + Schema::table('scheduled_database_backup_executions', function (Blueprint $table) { + $table->string('database_name')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('scheduled_database_backups', function (Blueprint $table) { + $table->dropColumn('databases_to_backup'); + }); + Schema::table('scheduled_database_backup_executions', function (Blueprint $table) { + $table->dropColumn('database_name'); + }); + } +}; diff --git a/database/seeders/StandaloneRedisSeeder.php b/database/seeders/StandaloneRedisSeeder.php new file mode 100644 index 000000000..cbe10bb00 --- /dev/null +++ b/database/seeders/StandaloneRedisSeeder.php @@ -0,0 +1,23 @@ + 'Local PostgreSQL', + 'description' => 'Local PostgreSQL for testing', + 'redis_password' => 'redis', + 'environment_id' => 1, + 'destination_id' => 0, + 'destination_type' => StandaloneDocker::class, + ]); + } +} diff --git a/resources/views/components/databases/navbar.blade.php b/resources/views/components/databases/navbar.blade.php index 6681565ba..84eb2479e 100644 --- a/resources/views/components/databases/navbar.blade.php +++ b/resources/views/components/databases/navbar.blade.php @@ -7,14 +7,13 @@ href="{{ route('project.database.logs', $parameters) }}"> - - - - {{-- --}} + @if ($database->getMorphClass() === 'App\Models\StandalonePostgresql') + + + + @endif
- {{-- --}} - @if ($database->status !== 'exited')