diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index e7ba6e7171..09797ca437 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -24,7 +24,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip + extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, pgsql, tokenizer, xml, zip tools: composer:v2 coverage: none diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2cbc4feb17..b3307ee111 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -61,7 +61,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip + extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, pgsql, tokenizer, xml, zip tools: composer:v2 coverage: none @@ -134,7 +134,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip + extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, pgsql, tokenizer, xml, zip tools: composer:v2 coverage: none @@ -195,7 +195,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip + extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, pgsql, tokenizer, xml, zip tools: composer:v2 coverage: none diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 0b767fcf78..cb46e5a2b1 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -3,7 +3,7 @@ name: Lint on: pull_request: branches: - - '**' + - "**" jobs: pint: @@ -17,7 +17,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: "8.3" - extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip + extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, pgsql, tokenizer, xml, zip tools: composer:v2 coverage: none @@ -40,7 +40,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: "8.3" - extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip + extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, pgsql, tokenizer, xml, zip tools: composer:v2 coverage: none diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 9d76b2ad53..a0d167c6c1 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -20,7 +20,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip + extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, pgsql, tokenizer, xml, zip tools: composer:v2 coverage: none diff --git a/Dockerfile b/Dockerfile index 942fd01851..30b3fcd4f0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,9 +12,9 @@ RUN touch .env # Install dependencies RUN apk update && apk add --no-cache \ libpng-dev libjpeg-turbo-dev freetype-dev libzip-dev icu-dev \ - zip unzip curl \ + zip unzip curl libpq-dev postgresql \ caddy ca-certificates supervisor \ - && docker-php-ext-install bcmath gd intl zip opcache pcntl posix pdo_mysql + && docker-php-ext-install bcmath gd intl zip opcache pcntl posix pdo_mysql pdo_pgsql # Install dependencies with Composer COPY composer.json composer.lock ./ diff --git a/app/Console/Commands/Environment/DatabaseSettingsCommand.php b/app/Console/Commands/Environment/DatabaseSettingsCommand.php index 570df0f2e5..97f9815f30 100644 --- a/app/Console/Commands/Environment/DatabaseSettingsCommand.php +++ b/app/Console/Commands/Environment/DatabaseSettingsCommand.php @@ -15,6 +15,7 @@ class DatabaseSettingsCommand extends Command 'sqlite' => 'SQLite (recommended)', 'mariadb' => 'MariaDB', 'mysql' => 'MySQL', + 'pgsql' => 'PostgreSQL', ]; protected $description = 'Configure database settings for the Panel.'; @@ -181,6 +182,66 @@ public function handle(): int 'Database Path', env('DB_DATABASE', 'database.sqlite') ); + } elseif ($this->variables['DB_CONNECTION'] === 'pgsql') { + $this->output->note(__('commands.database_settings.DB_HOST_note')); + $this->variables['DB_HOST'] = $this->option('host') ?? $this->ask( + 'Database Host', + config('database.connections.pgsql.host', '127.0.0.1') + ); + + $this->variables['DB_PORT'] = $this->option('port') ?? $this->ask( + 'Database Port', + config('database.connections.pgsql.port', 5432) + ); + + $this->variables['DB_DATABASE'] = $this->option('database') ?? $this->ask( + 'Database Name', + config('database.connections.pgsql.database', 'panel') + ); + + $this->output->note(__('commands.database_settings.DB_USERNAME_note')); + $this->variables['DB_USERNAME'] = $this->option('username') ?? $this->ask( + 'Database Username', + config('database.connections.pgsql.username', 'pelican') + ); + + $askForPGSQLPassword = true; + if (!empty(config('database.connections.pgsql.password')) && $this->input->isInteractive()) { + $this->variables['DB_PASSWORD'] = config('database.connections.pgsql.password'); + $askForPGSQLPassword = $this->confirm(__('commands.database_settings.DB_PASSWORD_note')); + } + + if ($askForPGSQLPassword) { + $this->variables['DB_PASSWORD'] = $this->option('password') ?? $this->secret('Database Password'); + } + + try { + // Test connection + config()->set('database.connections._panel_command_test', [ + 'driver' => 'pgsql', + 'host' => $this->variables['DB_HOST'], + 'port' => $this->variables['DB_PORT'], + 'database' => $this->variables['DB_DATABASE'], + 'username' => $this->variables['DB_USERNAME'], + 'password' => $this->variables['DB_PASSWORD'], + 'charset' => 'UTF8', + 'collation' => 'en_US.UTF-8', + 'strict' => true, + ]); + + $this->database->connection('_panel_command_test')->getPdo(); + } catch (\PDOException $exception) { + $this->output->error(sprintf('Unable to connect to the PostgreSQL server using the provided credentials. The error returned was "%s".', $exception->getMessage())); + $this->output->error(__('commands.database_settings.DB_error_2')); + + if ($this->confirm(__('commands.database_settings.go_back'))) { + $this->database->disconnect('_panel_command_test'); + + return $this->handle(); + } + + return 1; + } } $this->writeToEnvironment($this->variables); diff --git a/app/Extensions/DynamicDatabaseConnection.php b/app/Extensions/DynamicDatabaseConnection.php index 147f78064e..5fb5d5ebbe 100644 --- a/app/Extensions/DynamicDatabaseConnection.php +++ b/app/Extensions/DynamicDatabaseConnection.php @@ -6,30 +6,42 @@ class DynamicDatabaseConnection { - public const DB_CHARSET = 'utf8'; - - public const DB_COLLATION = 'utf8_unicode_ci'; - - public const DB_DRIVER = 'mysql'; + public const DB_DEFAULTS = [ + "mysql" => [ + "DB_CHARSET" => 'utf8', + "DB_COLLATION" => 'utf8_unicode_ci', + "DEFAULT_DB" => 'mysql', + ], + "mariadb" => [ + "DB_CHARSET" => 'utf8', + "DB_COLLATION" => 'utf8_unicode_ci', + "DEFAULT_DB" => 'mysql', + ], + "pgsql" => [ + "DB_CHARSET" => 'utf8', + "DB_COLLATION" => 'en_US', + "DEFAULT_DB" => 'postgres', + ], + ]; /** * Adds a dynamic database connection entry to the runtime config. */ - public function set(string $connection, DatabaseHost|int $host, string $database = 'mysql'): void + public function set(string $connection, DatabaseHost|int $host, string $database = null): void { if (!$host instanceof DatabaseHost) { $host = DatabaseHost::query()->findOrFail($host); } config()->set('database.connections.' . $connection, [ - 'driver' => self::DB_DRIVER, + 'driver' => $host->driver, 'host' => $host->host, 'port' => $host->port, - 'database' => $database, + 'database' => $database === null ? self::DB_DEFAULTS[$host->driver]['DEFAULT_DB'] : $database, 'username' => $host->username, 'password' => $host->password, - 'charset' => self::DB_CHARSET, - 'collation' => self::DB_COLLATION, + 'charset' => self::DB_DEFAULTS[$host->driver]['DB_CHARSET'], + 'collation' => self::DB_DEFAULTS[$host->driver]['DB_COLLATION'], ]); } } diff --git a/app/Filament/Admin/Resources/DatabaseHostResource/Pages/CreateDatabaseHost.php b/app/Filament/Admin/Resources/DatabaseHostResource/Pages/CreateDatabaseHost.php index e1ed34befb..76c976a8fc 100644 --- a/app/Filament/Admin/Resources/DatabaseHostResource/Pages/CreateDatabaseHost.php +++ b/app/Filament/Admin/Resources/DatabaseHostResource/Pages/CreateDatabaseHost.php @@ -8,6 +8,7 @@ use Filament\Forms\Components\Section; use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; +use Filament\Forms\Components\ToggleButtons; use Filament\Forms\Form; use Filament\Notifications\Notification; use Filament\Resources\Pages\CreateRecord; @@ -40,6 +41,15 @@ public function form(Form $form): Form 'lg' => 4, ]) ->schema([ + ToggleButtons::make('driver') + ->label('Database Driver') + ->inline() + ->options([ + 'mariadb' => 'MariaDB', + 'mysql' => 'MySQL', + 'pgsql' => 'PostgreSQL', + ]) + ->default('mariadb'), TextInput::make('host') ->columnSpan(2) ->helperText('The IP address or Domain name that should be used when attempting to connect to this MySQL host from this Panel to create new databases.') diff --git a/app/Livewire/Installer/Steps/DatabaseStep.php b/app/Livewire/Installer/Steps/DatabaseStep.php index 2751d1a476..bba2091316 100644 --- a/app/Livewire/Installer/Steps/DatabaseStep.php +++ b/app/Livewire/Installer/Steps/DatabaseStep.php @@ -19,6 +19,7 @@ class DatabaseStep 'sqlite' => 'SQLite', 'mariadb' => 'MariaDB', 'mysql' => 'MySQL', + 'pgsql' => 'PostgreSQL', ]; public static function make(PanelInstaller $installer): Step @@ -39,15 +40,24 @@ public static function make(PanelInstaller $installer): Step ->afterStateUpdated(function ($state, Set $set, Get $get) { $set('env_database.DB_DATABASE', $state === 'sqlite' ? 'database.sqlite' : 'panel'); - if ($state === 'sqlite') { - $set('env_database.DB_HOST', null); - $set('env_database.DB_PORT', null); - $set('env_database.DB_USERNAME', null); - $set('env_database.DB_PASSWORD', null); - } else { - $set('env_database.DB_HOST', $get('env_database.DB_HOST') ?? '127.0.0.1'); - $set('env_database.DB_PORT', $get('env_database.DB_PORT') ?? '3306'); - $set('env_database.DB_USERNAME', $get('env_database.DB_USERNAME') ?? 'pelican'); + switch ($state) { + case 'sqlite': + $set('env_database.DB_HOST', null); + $set('env_database.DB_PORT', null); + $set('env_database.DB_USERNAME', null); + $set('env_database.DB_PASSWORD', null); + break; + case 'mariadb': + case 'mysql': + $set('env_database.DB_HOST', $get('env_database.DB_HOST') ?? '127.0.0.1'); + $set('env_database.DB_PORT', $get('env_database.DB_PORT') ?? '3306'); + $set('env_database.DB_USERNAME', $get('env_database.DB_USERNAME') ?? 'pelican'); + break; + case 'pgsql': + $set('env_database.DB_HOST', $get('env_database.DB_HOST') ?? '127.0.0.1'); + $set('env_database.DB_PORT', $get('env_database.DB_PORT') ?? '5432'); + $set('env_database.DB_USERNAME', $get('env_database.DB_USERNAME') ?? 'pelican'); + break; } }), TextInput::make('env_database.DB_DATABASE') @@ -102,34 +112,66 @@ public static function make(PanelInstaller $installer): Step private static function testConnection(string $driver, ?string $host, null|string|int $port, ?string $database, ?string $username, ?string $password): bool { - if ($driver === 'sqlite') { - return true; - } + switch ($driver) { + case 'sqlite': + return true; + + case 'mariadb': + case 'mysql': + try { + config()->set('database.connections._panel_install_test', [ + 'driver' => $driver, + 'host' => $host, + 'port' => $port, + 'database' => $database, + 'username' => $username, + 'password' => $password, + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'strict' => true, + ]); + + DB::connection('_panel_install_test')->getPdo(); + } catch (Exception $exception) { + DB::disconnect('_panel_install_test'); + + Notification::make() + ->title('Database connection failed') + ->body($exception->getMessage()) + ->danger() + ->send(); + + return false; + } + break; + + case 'pgsql': + try { + config()->set('database.connections._panel_install_test', [ + 'driver' => $driver, + 'host' => $host, + 'port' => $port, + 'database' => $database, + 'username' => $username, + 'password' => $password, + 'charset' => 'UTF8', + 'collation' => 'en_US.UTF-8', + 'strict' => true, + ]); + + DB::connection('_panel_install_test')->getPdo(); + } catch (Exception $exception) { + DB::disconnect('_panel_install_test'); - try { - config()->set('database.connections._panel_install_test', [ - 'driver' => $driver, - 'host' => $host, - 'port' => $port, - 'database' => $database, - 'username' => $username, - 'password' => $password, - 'charset' => 'utf8mb4', - 'collation' => 'utf8mb4_unicode_ci', - 'strict' => true, - ]); - - DB::connection('_panel_install_test')->getPdo(); - } catch (Exception $exception) { - DB::disconnect('_panel_install_test'); - - Notification::make() - ->title('Database connection failed') - ->body($exception->getMessage()) - ->danger() - ->send(); - - return false; + Notification::make() + ->title('Database connection failed') + ->body($exception->getMessage()) + ->danger() + ->send(); + + return false; + } + break; } return true; diff --git a/app/Livewire/Installer/Steps/RequirementsStep.php b/app/Livewire/Installer/Steps/RequirementsStep.php index 258f76cab5..93164894c9 100644 --- a/app/Livewire/Installer/Steps/RequirementsStep.php +++ b/app/Livewire/Installer/Steps/RequirementsStep.php @@ -34,6 +34,7 @@ public static function make(): Step 'intl' => extension_loaded('intl'), 'mbstring' => extension_loaded('mbstring'), 'MySQL' => extension_loaded('pdo_mysql'), + 'PGSQL' => extension_loaded('pdo_pgsql'), 'SQLite3' => extension_loaded('pdo_sqlite'), 'XML' => extension_loaded('xml'), 'Zip' => extension_loaded('zip'), diff --git a/app/Models/Database.php b/app/Models/Database.php index 795fa28aff..18daf1a93d 100644 --- a/app/Models/Database.php +++ b/app/Models/Database.php @@ -31,6 +31,14 @@ class Database extends Model public const DEFAULT_CONNECTION_NAME = 'dynamic'; + public const DATABASE_SETUP_CONNECTION_NAME = '_panel_setup_database'; + + public const JDBC_DRIVER_MAPPING = [ + 'mysql' => 'mysql', + 'mariadb' => 'mariadb', + 'pgsql' => 'postgresql', + ]; + /** * The table associated with the model. */ @@ -92,7 +100,7 @@ public function server(): BelongsTo protected function jdbc(): Attribute { return Attribute::make( - get: fn () => 'jdbc:mysql://' . $this->username . ':' . urlencode($this->password) . '@' . $this->host->host . ':' . $this->host->port . '/' . $this->database, + get: fn () => 'jdbc:' . self::JDBC_DRIVER_MAPPING[$this->host->driver] . '://' . $this->username . ':' . urlencode($this->password) . '@' . $this->host->host . ':' . $this->host->port . '/' . $this->database, ); } @@ -104,28 +112,94 @@ private function run(string $statement): bool return DB::connection(self::DEFAULT_CONNECTION_NAME)->statement($statement); } + /** + * Setup a temporary database connection. + * Needed for PostgreSQL connections. + */ + private function setConfigDatabase(string $database): void + { + config()->set('database.connections.' . self::DATABASE_SETUP_CONNECTION_NAME, [ + 'driver' => $this->host->driver, + 'host' => $this->host->host, + 'port' => $this->host->port, + 'database' => $database, + 'username' => $this->host->username, + 'password' => $this->host->password, + 'charset' => 'UTF8', + 'collation' => 'en_US.UTF-8', + 'strict' => true, + ]); + } + /** * Create a new database on a given connection. */ public function createDatabase(string $database): bool { - return $this->run(sprintf('CREATE DATABASE IF NOT EXISTS `%s`', $database)); + switch ($this->host->driver) { + case 'mysql': + case 'mariadb': + return $this->run(sprintf('CREATE DATABASE IF NOT EXISTS `%s`', $database)); + case 'pgsql': + return $this->run(sprintf('CREATE DATABASE "%s"', $database)); + } + return false; } /** * Create a new database user on a given connection. */ - public function createUser(string $username, string $remote, string $password, ?int $max_connections): bool + public function createUser(string $database, string $username, string $remote, string $password, ?int $max_connections): bool { - $args = [$username, $remote, $password]; - $command = 'CREATE USER `%s`@`%s` IDENTIFIED BY \'%s\''; + $args = []; + $command = ''; + switch ($this->host->driver) { + case 'mysql': + case 'mariadb': + $args = [$username, $remote, $password]; + $command = 'CREATE USER `%s`@`%s` IDENTIFIED BY \'%s\''; + + if (!empty($max_connections)) { + $args[] = $max_connections; + $command .= ' WITH MAX_USER_CONNECTIONS %s'; + } + return $this->run(sprintf($command, ...$args)); + case 'pgsql': + try { + $this->setConfigDatabase($database); + $args = [$username, $password]; + $command = 'CREATE USER "%s" WITH PASSWORD \'%s\''; - if (!empty($max_connections)) { - $args[] = $max_connections; - $command .= ' WITH MAX_USER_CONNECTIONS %s'; + if (!empty($max_connections)) { + $args[] = $max_connections; + $command .= ' CONNECTION LIMIT %s'; + } + return DB::connection(self::DATABASE_SETUP_CONNECTION_NAME)->statement(sprintf($command, ...$args)); + } finally { + DB::disconnect(self::DATABASE_SETUP_CONNECTION_NAME); + } + return false; } + } - return $this->run(sprintf($command, ...$args)); + /** + * Updates the user's password on a given connection. + * Only implemented for PostgreSQL. + */ + public function updateUserPassword(string $database, string $username, string $remote, string $password): bool + { + switch ($this->host->driver) { + case 'pgsql': + try { + $this->setConfigDatabase($database); + return DB::connection(self::DATABASE_SETUP_CONNECTION_NAME)->statement(sprintf('ALTER USER "%s" WITH PASSWORD \'%s\'', $username, $password)); + } finally { + DB::disconnect(self::DATABASE_SETUP_CONNECTION_NAME); + } + return false; + default: + throw new BadMethodCallException('updateUserPassword only implemented for PostgreSQL'); + } } /** @@ -133,12 +207,31 @@ public function createUser(string $username, string $remote, string $password, ? */ public function assignUserToDatabase(string $database, string $username, string $remote): bool { - return $this->run(sprintf( - 'GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, REFERENCES, INDEX, LOCK TABLES, CREATE ROUTINE, ALTER ROUTINE, EXECUTE, CREATE TEMPORARY TABLES, CREATE VIEW, SHOW VIEW, EVENT, TRIGGER ON `%s`.* TO `%s`@`%s`', - $database, - $username, - $remote - )); + switch ($this->host->driver) { + case 'mysql': + case 'mariadb': + return $this->run(sprintf( + 'GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, REFERENCES, INDEX, LOCK TABLES, CREATE ROUTINE, ALTER ROUTINE, EXECUTE, CREATE TEMPORARY TABLES, CREATE VIEW, SHOW VIEW, EVENT, TRIGGER ON `%s`.* TO `%s`@`%s`', + $database, + $username, + $remote + )); + case 'pgsql': + try { + $this->setConfigDatabase($database); + $success = DB::connection(self::DATABASE_SETUP_CONNECTION_NAME)->statement(sprintf('REVOKE CONNECT ON DATABASE "%s" FROM public', $database)); + $success = DB::connection(self::DATABASE_SETUP_CONNECTION_NAME)->statement(sprintf('GRANT CONNECT ON DATABASE "%s" TO "%s"', $database, $username)); + $success = DB::connection(self::DATABASE_SETUP_CONNECTION_NAME)->statement('DROP SCHEMA public'); + $success = $success && DB::connection(self::DATABASE_SETUP_CONNECTION_NAME)->statement(sprintf( + 'CREATE SCHEMA AUTHORIZATION "%s"', + $username + )); + return $success; + } finally { + DB::disconnect(self::DATABASE_SETUP_CONNECTION_NAME); + } + return false; + } } /** @@ -146,7 +239,13 @@ public function assignUserToDatabase(string $database, string $username, string */ public function flush(): bool { - return $this->run('FLUSH PRIVILEGES'); + switch ($this->host->driver) { + case 'mysql': + case 'mariadb': + return $this->run('FLUSH PRIVILEGES'); + case 'pgsql': + return true; + } } /** @@ -154,7 +253,15 @@ public function flush(): bool */ public function dropDatabase(string $database): bool { - return $this->run(sprintf('DROP DATABASE IF EXISTS `%s`', $database)); + switch ($this->host->driver) { + case 'mysql': + case 'mariadb': + return $this->run(sprintf('DROP DATABASE IF EXISTS `%s`', $database)); + case 'pgsql': + $success = $this->run(sprintf('SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = \'%s\' AND pid <> pg_backend_pid()', $database)); + $success = $success && $this->run(sprintf('DROP DATABASE IF EXISTS "%s"', $database)); + return $success; + } } /** @@ -162,6 +269,12 @@ public function dropDatabase(string $database): bool */ public function dropUser(string $username, string $remote): bool { - return $this->run(sprintf('DROP USER IF EXISTS `%s`@`%s`', $username, $remote)); + switch ($this->host->driver) { + case 'mysql': + case 'mariadb': + return $this->run(sprintf('DROP USER IF EXISTS `%s`@`%s`', $username, $remote)); + case 'pgsql': + return $this->run(sprintf('DROP USER IF EXISTS "%s"', $username)); + } } } diff --git a/app/Models/DatabaseHost.php b/app/Models/DatabaseHost.php index 1163959d25..61022adf18 100644 --- a/app/Models/DatabaseHost.php +++ b/app/Models/DatabaseHost.php @@ -20,6 +20,7 @@ * @property int|null $nodes_count * @property \Illuminate\Database\Eloquent\Collection|\App\Models\Database[] $databases * @property int|null $databases_count + * @property string $driver */ class DatabaseHost extends Model { @@ -43,7 +44,7 @@ class DatabaseHost extends Model * Fields that are mass assignable. */ protected $fillable = [ - 'name', 'host', 'port', 'username', 'password', 'max_databases', + 'name', 'host', 'port', 'username', 'password', 'max_databases', 'driver', ]; /** @@ -57,6 +58,7 @@ class DatabaseHost extends Model 'password' => 'nullable|string', 'node_ids' => 'nullable|array', 'node_ids.*' => 'required|integer,exists:nodes,id', + 'driver' => 'required|string' ]; protected function casts(): array diff --git a/app/Services/Databases/DatabaseManagementService.php b/app/Services/Databases/DatabaseManagementService.php index 8ec783328e..8221ba0cf3 100644 --- a/app/Services/Databases/DatabaseManagementService.php +++ b/app/Services/Databases/DatabaseManagementService.php @@ -95,6 +95,7 @@ public function create(Server $server, array $data): Database $database->createDatabase($database->database); $database->createUser( + $database->database, $database->username, $database->remote, $database->password, diff --git a/app/Services/Databases/DatabasePasswordService.php b/app/Services/Databases/DatabasePasswordService.php index 110c5abe5c..7ec01d784e 100644 --- a/app/Services/Databases/DatabasePasswordService.php +++ b/app/Services/Databases/DatabasePasswordService.php @@ -35,10 +35,18 @@ public function handle(Database|int $database): void 'password' => $password, ]); - $database->dropUser($database->username, $database->remote); - $database->createUser($database->username, $database->remote, $password, $database->max_connections); - $database->assignUserToDatabase($database->database, $database->username, $database->remote); - $database->flush(); + switch ($database->host->driver) { + case 'mysql': + case 'mariadb': + $database->dropUser($database->username, $database->remote); + $database->createUser($database->database, $database->username, $database->remote, $password, $database->max_connections); + $database->assignUserToDatabase($database->database, $database->username, $database->remote); + $database->flush(); + break; + case 'pgsql': + $database->updateUserPassword($database->database, $database->username, $database->remote, $password); + break; + } }); } } diff --git a/app/Services/Databases/Hosts/HostCreationService.php b/app/Services/Databases/Hosts/HostCreationService.php index ff38cb5c97..f0f340b730 100644 --- a/app/Services/Databases/Hosts/HostCreationService.php +++ b/app/Services/Databases/Hosts/HostCreationService.php @@ -27,6 +27,7 @@ public function handle(array $data): DatabaseHost { return $this->connection->transaction(function () use ($data) { $host = DatabaseHost::query()->create([ + 'driver' => array_get($data, 'driver'), 'password' => array_get($data, 'password'), 'name' => array_get($data, 'name'), 'host' => array_get($data, 'host'), diff --git a/app/Transformers/Api/Client/DatabaseTransformer.php b/app/Transformers/Api/Client/DatabaseTransformer.php index 1a38559391..ba50e29378 100644 --- a/app/Transformers/Api/Client/DatabaseTransformer.php +++ b/app/Transformers/Api/Client/DatabaseTransformer.php @@ -21,6 +21,7 @@ public function transform(Database $model): array $model->loadMissing('host'); return [ + 'driver' => $model->getRelation('host')->driver, 'id' => $model->id, 'host' => [ 'address' => $model->getRelation('host')->host, diff --git a/compose.yml b/compose.yml index 5b55249a22..a8007c653a 100644 --- a/compose.yml +++ b/compose.yml @@ -26,6 +26,19 @@ x-common: # services: + pgsql: + image: postgres:17 + restart: always + networks: + - default + ports: + - "5432:5432" + environment: + POSTGRES_PASSWORD: password + POSTGRES_USER: pelican + POSTGRES_DB: panel + volumes: + - pelican-pgsql:/var/lib/postgresql/data panel: image: ghcr.io/pelican-dev/panel:latest build: @@ -50,6 +63,7 @@ services: volumes: pelican-data: pelican-logs: + pelican-pgsql: networks: default: diff --git a/config/database.php b/config/database.php index bc995eddac..58bc48cfae 100644 --- a/config/database.php +++ b/config/database.php @@ -58,6 +58,22 @@ PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), ]) : [], ], + + 'pgsql' => [ + 'driver' => 'pgsql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '5432'), + 'database' => env('DB_DATABASE', 'panel'), + 'username' => env('DB_USERNAME', 'pelican'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'UTF8'), + 'prefix' => env('DB_PREFIX', ''), + 'prefix_indexes' => true, + 'strict' => env('DB_STRICT_MODE', false), + 'engine' => null, + 'sslmode' => 'prefer', + ], ], 'migrations' => [ diff --git a/database/migrations/2016_09_04_182835_create_notifications_table.php b/database/migrations/2016_09_04_182835_create_notifications_table.php index 788e37be07..14a6e8b758 100644 --- a/database/migrations/2016_09_04_182835_create_notifications_table.php +++ b/database/migrations/2016_09_04_182835_create_notifications_table.php @@ -14,7 +14,16 @@ public function up(): void $table->string('id')->primary(); $table->string('type'); $table->morphs('notifiable'); - $table->text('data'); + switch (Schema::getConnection()->getDriverName()) { + case 'sqlite': + case 'mysql': + case 'mariadb': + $table->text('data'); + break; + case 'pgsql': + $table->jsonb('data'); + break; + } $table->timestamp('read_at')->nullable(); $table->timestamps(); }); diff --git a/database/migrations/2017_02_02_175548_UpdateColumnNames.php b/database/migrations/2017_02_02_175548_UpdateColumnNames.php index 9fbae33527..f18db933e7 100644 --- a/database/migrations/2017_02_02_175548_UpdateColumnNames.php +++ b/database/migrations/2017_02_02_175548_UpdateColumnNames.php @@ -19,13 +19,16 @@ public function up(): void $table->dropForeign(['option']); $table->dropForeign(['pack']); - if (Schema::getConnection()->getDriverName() !== 'sqlite') { - $table->dropIndex('servers_node_foreign'); - $table->dropIndex('servers_owner_foreign'); - $table->dropIndex('servers_allocation_foreign'); - $table->dropIndex('servers_service_foreign'); - $table->dropIndex('servers_option_foreign'); - $table->dropIndex('servers_pack_foreign'); + switch (Schema::getConnection()->getDriverName()) { + case 'mariadb': + case 'mysql': + $table->dropIndex('servers_node_foreign'); + $table->dropIndex('servers_owner_foreign'); + $table->dropIndex('servers_allocation_foreign'); + $table->dropIndex('servers_service_foreign'); + $table->dropIndex('servers_option_foreign'); + $table->dropIndex('servers_pack_foreign'); + break; } $table->renameColumn('node', 'node_id'); diff --git a/database/migrations/2017_02_03_140948_UpdateNodesTable.php b/database/migrations/2017_02_03_140948_UpdateNodesTable.php index 07186787b3..4673f644f6 100644 --- a/database/migrations/2017_02_03_140948_UpdateNodesTable.php +++ b/database/migrations/2017_02_03_140948_UpdateNodesTable.php @@ -14,8 +14,11 @@ public function up(): void Schema::table('nodes', function (Blueprint $table) { $table->dropForeign(['location']); - if (Schema::getConnection()->getDriverName() !== 'sqlite') { - $table->dropIndex('nodes_location_foreign'); + switch (Schema::getConnection()->getDriverName()) { + case 'mariadb': + case 'mysql': + $table->dropIndex('nodes_location_foreign'); + break; } $table->renameColumn('location', 'location_id'); @@ -31,8 +34,11 @@ public function down(): void Schema::table('nodes', function (Blueprint $table) { $table->dropForeign(['location_id']); - if (Schema::getConnection()->getDriverName() !== 'sqlite') { - $table->dropIndex('nodes_location_id_foreign'); + switch (Schema::getConnection()->getDriverName()) { + case 'mariadb': + case 'mysql': + $table->dropIndex('nodes_location_id_foreign'); + break; } $table->renameColumn('location_id', 'location'); diff --git a/database/migrations/2017_02_03_155554_RenameColumns.php b/database/migrations/2017_02_03_155554_RenameColumns.php index e61b72ab3b..362823df86 100644 --- a/database/migrations/2017_02_03_155554_RenameColumns.php +++ b/database/migrations/2017_02_03_155554_RenameColumns.php @@ -15,9 +15,12 @@ public function up(): void $table->dropForeign(['node']); $table->dropForeign(['assigned_to']); - if (Schema::getConnection()->getDriverName() !== 'sqlite') { - $table->dropIndex('allocations_node_foreign'); - $table->dropIndex('allocations_assigned_to_foreign'); + switch (Schema::getConnection()->getDriverName()) { + case 'mariadb': + case 'mysql': + $table->dropIndex('allocations_node_foreign'); + $table->dropIndex('allocations_assigned_to_foreign'); + break; } $table->renameColumn('node', 'node_id'); @@ -36,9 +39,12 @@ public function down(): void $table->dropForeign(['node_id']); $table->dropForeign(['server_id']); - if (Schema::getConnection()->getDriverName() !== 'sqlite') { - $table->dropIndex('allocations_node_id_foreign'); - $table->dropIndex('allocations_server_id_foreign'); + switch (Schema::getConnection()->getDriverName()) { + case 'mariadb': + case 'mysql': + $table->dropIndex('allocations_node_id_foreign'); + $table->dropIndex('allocations_server_id_foreign'); + break; } $table->renameColumn('node_id', 'node'); diff --git a/database/migrations/2017_02_05_164123_AdjustColumnNames.php b/database/migrations/2017_02_05_164123_AdjustColumnNames.php index 275eccaea0..9b63d08fce 100644 --- a/database/migrations/2017_02_05_164123_AdjustColumnNames.php +++ b/database/migrations/2017_02_05_164123_AdjustColumnNames.php @@ -14,8 +14,11 @@ public function up(): void Schema::table('service_options', function (Blueprint $table) { $table->dropForeign(['parent_service']); - if (Schema::getConnection()->getDriverName() !== 'sqlite') { - $table->dropIndex('service_options_parent_service_foreign'); + switch (Schema::getConnection()->getDriverName()) { + case 'mariadb': + case 'mysql': + $table->dropIndex('service_options_parent_service_foreign'); + break; } $table->renameColumn('parent_service', 'service_id'); @@ -31,8 +34,11 @@ public function down(): void Schema::table('service_options', function (Blueprint $table) { $table->dropForeign(['service_id']); - if (Schema::getConnection()->getDriverName() !== 'sqlite') { - $table->dropIndex('service_options_service_id_foreign'); + switch (Schema::getConnection()->getDriverName()) { + case 'mariadb': + case 'mysql': + $table->dropIndex('service_options_service_id_foreign'); + break; } $table->renameColumn('service_id', 'parent_service'); diff --git a/database/migrations/2017_02_05_164516_AdjustColumnNamesForServicePacks.php b/database/migrations/2017_02_05_164516_AdjustColumnNamesForServicePacks.php index 5369b68e7e..9434a5fda1 100644 --- a/database/migrations/2017_02_05_164516_AdjustColumnNamesForServicePacks.php +++ b/database/migrations/2017_02_05_164516_AdjustColumnNamesForServicePacks.php @@ -14,8 +14,11 @@ public function up(): void Schema::table('service_packs', function (Blueprint $table) { $table->dropForeign(['option']); - if (Schema::getConnection()->getDriverName() !== 'sqlite') { - $table->dropIndex('service_packs_option_foreign'); + switch (Schema::getConnection()->getDriverName()) { + case 'mariadb': + case 'mysql': + $table->dropIndex('service_packs_option_foreign'); + break; } $table->renameColumn('option', 'option_id'); @@ -31,8 +34,11 @@ public function down(): void Schema::table('service_packs', function (Blueprint $table) { $table->dropForeign(['option_id']); - if (Schema::getConnection()->getDriverName() !== 'sqlite') { - $table->dropIndex('service_packs_option_id_foreign'); + switch (Schema::getConnection()->getDriverName()) { + case 'mariadb': + case 'mysql': + $table->dropIndex('service_packs_option_id_foreign'); + break; } $table->renameColumn('option_id', 'option'); diff --git a/database/migrations/2017_02_09_174834_SetupPermissionsPivotTable.php b/database/migrations/2017_02_09_174834_SetupPermissionsPivotTable.php index e1af0b0a31..dade3cdb75 100644 --- a/database/migrations/2017_02_09_174834_SetupPermissionsPivotTable.php +++ b/database/migrations/2017_02_09_174834_SetupPermissionsPivotTable.php @@ -31,12 +31,16 @@ public function up(): void $table->dropForeign(['server_id']); $table->dropForeign(['user_id']); - if (Schema::getConnection()->getDriverName() !== 'sqlite') { - $table->dropIndex('permissions_server_id_foreign'); - $table->dropIndex('permissions_user_id_foreign'); - } else { - $table->dropForeign(['server_id']); - $table->dropForeign(['user_id']); + switch (Schema::getConnection()->getDriverName()) { + case 'mariadb': + case 'mysql': + $table->dropIndex('permissions_server_id_foreign'); + $table->dropIndex('permissions_user_id_foreign'); + break; + case 'sqlite': + $table->dropForeign(['server_id']); + $table->dropForeign(['user_id']); + break; } $table->dropColumn('server_id'); @@ -68,9 +72,12 @@ public function down(): void }); Schema::table('permissions', function (Blueprint $table) { - if (Schema::getConnection()->getDriverName() !== 'sqlite') { - $table->dropForeign('permissions_subuser_id_foreign'); - $table->dropIndex('permissions_subuser_id_foreign'); + switch (Schema::getConnection()->getDriverName()) { + case 'mariadb': + case 'mysql': + $table->dropForeign('permissions_subuser_id_foreign'); + $table->dropIndex('permissions_subuser_id_foreign'); + break; } $table->dropColumn('subuser_id'); diff --git a/database/migrations/2017_02_10_171858_UpdateAPIKeyColumnNames.php b/database/migrations/2017_02_10_171858_UpdateAPIKeyColumnNames.php index 177d599525..35d9ef7d10 100644 --- a/database/migrations/2017_02_10_171858_UpdateAPIKeyColumnNames.php +++ b/database/migrations/2017_02_10_171858_UpdateAPIKeyColumnNames.php @@ -12,9 +12,12 @@ public function up(): void { Schema::table('api_keys', function (Blueprint $table) { - if (Schema::getConnection()->getDriverName() !== 'sqlite') { - $table->dropForeign('api_keys_user_foreign'); - $table->dropIndex('api_keys_user_foreign'); + switch (Schema::getConnection()->getDriverName()) { + case 'mariadb': + case 'mysql': + $table->dropForeign('api_keys_user_foreign'); + $table->dropIndex('api_keys_user_foreign'); + break; } $table->renameColumn('user', 'user_id'); @@ -28,9 +31,12 @@ public function up(): void public function down(): void { Schema::table('api_keys', function (Blueprint $table) { - if (Schema::getConnection()->getDriverName() !== 'sqlite') { - $table->dropForeign('api_keys_user_id_foreign'); - $table->dropIndex('api_keys_user_id_foreign'); + switch (Schema::getConnection()->getDriverName()) { + case 'mariadb': + case 'mysql': + $table->dropForeign('api_keys_user_id_foreign'); + $table->dropIndex('api_keys_user_id_foreign'); + break; } $table->renameColumn('user_id', 'user'); diff --git a/database/migrations/2017_11_19_122708_MigratePubPrivFormatToSingleKey.php b/database/migrations/2017_11_19_122708_MigratePubPrivFormatToSingleKey.php index 7cc32110a9..9606184d01 100644 --- a/database/migrations/2017_11_19_122708_MigratePubPrivFormatToSingleKey.php +++ b/database/migrations/2017_11_19_122708_MigratePubPrivFormatToSingleKey.php @@ -28,17 +28,26 @@ public function up(): void }); }); - if (Schema::getConnection()->getDriverName() === 'sqlite') { - Schema::table('api_keys', function (Blueprint $table) { - $table->dropColumn('public'); - $table->char('secret', 32)->change(); - $table->renameColumn('secret', 'token'); - $table->string('token', 32)->unique()->change(); - }); - } else { - DB::statement('ALTER TABLE `api_keys` CHANGE `secret` `token` CHAR(32) NOT NULL, ADD UNIQUE INDEX `api_keys_token_unique` (`token`(32))'); + switch (Schema::getConnection()->getDriverName()) { + case 'sqlite': + Schema::table('api_keys', function (Blueprint $table) { + $table->dropColumn('public'); + $table->char('secret', 32)->change(); + $table->renameColumn('secret', 'token'); + $table->string('token', 32)->unique()->change(); + }); + break; + case 'mariadb': + case 'mysql': + DB::statement('ALTER TABLE `api_keys` DROP COLUMN `public`'); + DB::statement('ALTER TABLE `api_keys` CHANGE `secret` `token` CHAR(32) NOT NULL, ADD UNIQUE INDEX `api_keys_token_unique` (`token`(32))'); + break; + case 'pgsql': + DB::statement('ALTER TABLE api_keys DROP COLUMN public'); + DB::statement('ALTER TABLE api_keys RENAME COLUMN secret TO token'); + DB::statement('ALTER TABLE api_keys ALTER COLUMN token SET DATA TYPE CHAR(32), ADD CONSTRAINT api_keys_token_unique UNIQUE (token)'); + break; } - } /** @@ -46,7 +55,18 @@ public function up(): void */ public function down(): void { - DB::statement('ALTER TABLE `api_keys` CHANGE `token` `secret` TEXT, DROP INDEX `api_keys_token_unique`'); + + switch (Schema::getConnection()->getDriverName()) { + case 'sqlite': + case 'mariadb': + case 'mysql': + DB::statement('ALTER TABLE `api_keys` CHANGE `token` `secret` TEXT, DROP INDEX `api_keys_token_unique`'); + break; + case 'pgsql': + DB::statement('ALTER TABLE api_keys RENAME COLUMN token TO secret, ALTER COLUMN secret SET DATA TYPE TEXT, DROP CONSTRAINT api_keys_token_unique'); + break; + } + Schema::table('api_keys', function (Blueprint $table) { $table->char('public', 16)->after('user_id'); diff --git a/database/migrations/2020_03_22_163911_merge_permissions_table_into_subusers.php b/database/migrations/2020_03_22_163911_merge_permissions_table_into_subusers.php index 6a9280b5f0..923cadf3a2 100644 --- a/database/migrations/2020_03_22_163911_merge_permissions_table_into_subusers.php +++ b/database/migrations/2020_03_22_163911_merge_permissions_table_into_subusers.php @@ -65,12 +65,21 @@ public function up(): void { Schema::table('subusers', function (Blueprint $table) { - $table->json('permissions')->nullable()->after('server_id'); + switch (Schema::getConnection()->getDriverName()) { + case 'mysql': + case 'mariadb': + case 'sqlite': + $table->json('permissions')->nullable()->after('server_id'); + break; + case 'pgsql': + $table->jsonb('permissions')->nullable()->after('server_id'); + break; + } }); $cursor = DB::table('permissions') ->select(['subuser_id']) - ->selectRaw('GROUP_CONCAT(permission) as permissions') + ->selectRaw(Schema::getConnection()->getDriverName() === 'pgsql' ? 'STRING_AGG(permission,\',\') AS permissions' : 'GROUP_CONCAT(permission) as permissions') ->from('permissions') ->groupBy(['subuser_id']) ->cursor(); diff --git a/database/migrations/2020_11_02_201014_add_features_column_to_eggs.php b/database/migrations/2020_11_02_201014_add_features_column_to_eggs.php index e3d750f826..5ff59d9b5c 100644 --- a/database/migrations/2020_11_02_201014_add_features_column_to_eggs.php +++ b/database/migrations/2020_11_02_201014_add_features_column_to_eggs.php @@ -12,7 +12,16 @@ public function up(): void { Schema::table('eggs', function (Blueprint $table) { - $table->json('features')->after('description')->nullable(); + switch (Schema::getConnection()->getDriverName()) { + case 'mysql': + case 'mariadb': + case 'sqlite': + $table->json('features')->after('description')->nullable(); + break; + case 'pgsql': + $table->jsonb('features')->after('description')->nullable(); + break; + } }); } diff --git a/database/migrations/2020_12_12_102435_support_multiple_docker_images_and_updates.php b/database/migrations/2020_12_12_102435_support_multiple_docker_images_and_updates.php index 9e84f8e1d8..77b188b59c 100644 --- a/database/migrations/2020_12_12_102435_support_multiple_docker_images_and_updates.php +++ b/database/migrations/2020_12_12_102435_support_multiple_docker_images_and_updates.php @@ -13,12 +13,30 @@ public function up(): void { Schema::table('eggs', function (Blueprint $table) { - $table->json('docker_images')->after('docker_image')->nullable(); + switch (Schema::getConnection()->getDriverName()) { + case 'mysql': + case 'mariadb': + case 'sqlite': + $table->json('docker_images')->after('docker_image')->nullable(); + break; + case 'pgsql': + $table->jsonb('docker_images')->after('docker_image')->nullable(); + break; + } $table->text('update_url')->after('docker_images')->nullable(); }); Schema::table('eggs', function (Blueprint $table) { - DB::statement('UPDATE `eggs` SET `docker_images` = JSON_ARRAY(docker_image)'); + switch (Schema::getConnection()->getDriverName()) { + case 'sqlite': + case 'mariadb': + case 'mysql': + DB::statement('UPDATE `eggs` SET `docker_images` = JSON_ARRAY(docker_image)'); + break; + case 'pgsql': + DB::statement('UPDATE eggs SET docker_images = to_jsonb(ARRAY[docker_image])'); + break; + } }); Schema::table('eggs', function (Blueprint $table) { @@ -36,7 +54,16 @@ public function down(): void }); Schema::table('eggs', function (Blueprint $table) { - DB::statement('UPDATE `eggs` SET `docker_image` = JSON_UNQUOTE(JSON_EXTRACT(docker_images, "$[0]"))'); + switch (Schema::getConnection()->getDriverName()) { + case 'sqlite': + case 'mariadb': + case 'mysql': + DB::statement('UPDATE `eggs` SET `docker_image` = JSON_UNQUOTE(JSON_EXTRACT(docker_images, "$[0]"))'); + break; + case 'pgsql': + DB::statement('UPDATE eggs SET docker_image = (docker_images->>0)::text'); + break; + } }); Schema::table('eggs', function (Blueprint $table) { diff --git a/database/migrations/2020_12_14_013707_make_successful_nullable_in_server_transfers.php b/database/migrations/2020_12_14_013707_make_successful_nullable_in_server_transfers.php index c90e6a31d4..a77e8e1f22 100644 --- a/database/migrations/2020_12_14_013707_make_successful_nullable_in_server_transfers.php +++ b/database/migrations/2020_12_14_013707_make_successful_nullable_in_server_transfers.php @@ -11,9 +11,18 @@ */ public function up(): void { - Schema::table('server_transfers', function (Blueprint $table) { - $table->boolean('successful')->nullable()->default(null)->change(); - }); + switch (Schema::getConnection()->getDriverName()) { + case 'sqlite': + case 'mysql': + case 'mariadb': + Schema::table('server_transfers', function (Blueprint $table) { + $table->boolean('successful')->nullable()->default(null)->change(); + }); + break; + case 'pgsql': + DB::statement('ALTER TABLE server_transfers ALTER COLUMN successful TYPE boolean USING (successful::int::boolean), ALTER COLUMN successful DROP NOT NULL, ALTER COLUMN successful DROP DEFAULT, ALTER COLUMN successful DROP IDENTITY IF EXISTS'); + break; + } } /** diff --git a/database/migrations/2020_12_17_014330_add_archived_field_to_server_transfers_table.php b/database/migrations/2020_12_17_014330_add_archived_field_to_server_transfers_table.php index e11be7d3ea..f6f156aa13 100644 --- a/database/migrations/2020_12_17_014330_add_archived_field_to_server_transfers_table.php +++ b/database/migrations/2020_12_17_014330_add_archived_field_to_server_transfers_table.php @@ -18,7 +18,16 @@ public function up(): void // Update archived to all be true on existing transfers. Schema::table('server_transfers', function (Blueprint $table) { - DB::statement('UPDATE `server_transfers` SET `archived` = 1 WHERE `successful` = 1'); + switch (Schema::getConnection()->getDriverName()) { + case 'sqlite': + case 'mariadb': + case 'mysql': + DB::statement('UPDATE `server_transfers` SET `archived` = 1 WHERE `successful` = 1'); + break; + case 'pgsql': + DB::statement('UPDATE server_transfers SET archived = true WHERE successful = true'); + break; + } }); } diff --git a/database/migrations/2020_12_24_092449_make_allocation_fields_json.php b/database/migrations/2020_12_24_092449_make_allocation_fields_json.php index 1fb88bb051..e5e2eb8381 100644 --- a/database/migrations/2020_12_24_092449_make_allocation_fields_json.php +++ b/database/migrations/2020_12_24_092449_make_allocation_fields_json.php @@ -11,10 +11,20 @@ */ public function up(): void { - Schema::table('server_transfers', function (Blueprint $table) { - $table->json('old_additional_allocations')->nullable()->change(); - $table->json('new_additional_allocations')->nullable()->change(); - }); + switch (Schema::getConnection()->getDriverName()) { + case 'sqlite': + case 'mysql': + case 'mariadb': + Schema::table('server_transfers', function (Blueprint $table) { + $table->json('old_additional_allocations')->nullable()->change(); + $table->json('new_additional_allocations')->nullable()->change(); + }); + break; + case 'pgsql': + DB::statement('ALTER TABLE server_transfers ALTER COLUMN old_additional_allocations TYPE jsonb USING to_jsonb(old_additional_allocations)'); + DB::statement('ALTER TABLE server_transfers ALTER COLUMN new_additional_allocations TYPE jsonb USING to_jsonb(new_additional_allocations)'); + break; + } } /** diff --git a/database/migrations/2021_01_17_102401_create_audit_logs_table.php b/database/migrations/2021_01_17_102401_create_audit_logs_table.php index 43b02d63f3..7eb2b5e840 100644 --- a/database/migrations/2021_01_17_102401_create_audit_logs_table.php +++ b/database/migrations/2021_01_17_102401_create_audit_logs_table.php @@ -19,8 +19,18 @@ public function up(): void $table->unsignedInteger('server_id')->nullable(); $table->string('action'); $table->string('subaction')->nullable(); - $table->json('device'); - $table->json('metadata'); + switch (Schema::getConnection()->getDriverName()) { + case 'mysql': + case 'mariadb': + case 'sqlite': + $table->json('device'); + $table->json('metadata'); + break; + case 'pgsql': + $table->jsonb('device'); + $table->jsonb('metadata'); + break; + } $table->timestamp('created_at', 0); $table->foreign('user_id')->references('id')->on('users')->onDelete('set null'); diff --git a/database/migrations/2021_01_17_152623_add_generic_server_status_column.php b/database/migrations/2021_01_17_152623_add_generic_server_status_column.php index b864ee6286..a8340ca54d 100644 --- a/database/migrations/2021_01_17_152623_add_generic_server_status_column.php +++ b/database/migrations/2021_01_17_152623_add_generic_server_status_column.php @@ -17,9 +17,9 @@ public function up(): void }); DB::transaction(function () { - DB::update('UPDATE servers SET `status` = \'suspended\' WHERE `suspended` = 1'); - DB::update('UPDATE servers SET `status` = \'installing\' WHERE `installed` = 0'); - DB::update('UPDATE servers SET `status` = \'install_failed\' WHERE `installed` = 2'); + DB::update('UPDATE servers SET status = \'suspended\' WHERE suspended = 1'); + DB::update('UPDATE servers SET status = \'installing\' WHERE installed = 0'); + DB::update('UPDATE servers SET status = \'install_failed\' WHERE installed = 2'); }); Schema::table('servers', function (Blueprint $table) { @@ -39,9 +39,9 @@ public function down(): void }); DB::transaction(function () { - DB::update('UPDATE servers SET `suspended` = 1 WHERE `status` = \'suspended\''); - DB::update('UPDATE servers SET `installed` = 1 WHERE `status` IS NULL'); - DB::update('UPDATE servers SET `installed` = 2 WHERE `status` = \'install_failed\''); + DB::update('UPDATE servers SET suspended = 1 WHERE status = \'suspended\''); + DB::update('UPDATE servers SET installed = 1 WHERE status IS NULL'); + DB::update('UPDATE servers SET installed = 2 WHERE status = \'install_failed\''); }); Schema::table('servers', function (Blueprint $table) { diff --git a/database/migrations/2021_01_26_210502_update_file_denylist_to_json.php b/database/migrations/2021_01_26_210502_update_file_denylist_to_json.php index ee8b3a2f7d..cad4d8c5eb 100644 --- a/database/migrations/2021_01_26_210502_update_file_denylist_to_json.php +++ b/database/migrations/2021_01_26_210502_update_file_denylist_to_json.php @@ -16,7 +16,16 @@ public function up(): void }); Schema::table('eggs', function (Blueprint $table) { - $table->json('file_denylist')->nullable()->after('docker_images'); + switch (Schema::getConnection()->getDriverName()) { + case 'mysql': + case 'mariadb': + case 'sqlite': + $table->json('file_denylist')->nullable()->after('docker_images'); + break; + case 'pgsql': + $table->jsonb('file_denylist')->nullable()->after('docker_images'); + break; + } }); } diff --git a/database/migrations/2021_03_21_104718_force_cron_month_field_to_have_value_if_missing.php b/database/migrations/2021_03_21_104718_force_cron_month_field_to_have_value_if_missing.php index ada00daef4..283b5c652e 100644 --- a/database/migrations/2021_03_21_104718_force_cron_month_field_to_have_value_if_missing.php +++ b/database/migrations/2021_03_21_104718_force_cron_month_field_to_have_value_if_missing.php @@ -13,7 +13,7 @@ public function up(): void { Schema::table('schedules', function (Blueprint $table) { - DB::update("UPDATE `schedules` SET `cron_month` = '*' WHERE `cron_month` = ''"); + DB::update("UPDATE schedules SET cron_month = '*' WHERE cron_month = ''"); }); } diff --git a/database/migrations/2021_07_12_013420_remove_userinteraction.php b/database/migrations/2021_07_12_013420_remove_userinteraction.php index 25e0a15288..1ace8aeb9e 100644 --- a/database/migrations/2021_07_12_013420_remove_userinteraction.php +++ b/database/migrations/2021_07_12_013420_remove_userinteraction.php @@ -11,16 +11,38 @@ public function up(): void { // Remove User Interaction from startup config - DB::table('eggs')->update([ - 'config_startup' => DB::raw('JSON_REMOVE(config_startup, \'$.userInteraction\')'), - ]); + switch (Schema::getConnection()->getDriverName()) { + case 'sqlite': + case 'mysql': + case 'mariadb': + DB::table('eggs')->update([ + 'config_startup' => DB::raw('JSON_REMOVE(config_startup, \'$.userInteraction\')'), + ]); + break; + case 'pgsql': + DB::table('eggs')->update([ + 'config_startup' => DB::raw('(config_startup::jsonb - \'userInteraction\')::text'), + ]); + break; + } } public function down(): void { // Add blank User Interaction array back to startup config - DB::table('eggs')->update([ - 'config_startup' => DB::raw('JSON_SET(config_startup, \'$.userInteraction\', JSON_ARRAY())'), - ]); + switch (Schema::getConnection()->getDriverName()) { + case 'sqlite': + case 'mysql': + case 'mariadb': + DB::table('eggs')->update([ + 'config_startup' => DB::raw('JSON_SET(config_startup, \'$.userInteraction\', JSON_ARRAY())'), + ]); + break; + case 'pgsql': + DB::table('eggs')->update([ + 'config_startup' => DB::raw('jsonb_set(config_startup::jsonb, \'{userInteraction}\', \'[]\'::jsonb)::text'), + ]); + break; + } } }; diff --git a/database/migrations/2022_05_28_135717_create_activity_logs_table.php b/database/migrations/2022_05_28_135717_create_activity_logs_table.php index 186125aa36..51a5e0f142 100644 --- a/database/migrations/2022_05_28_135717_create_activity_logs_table.php +++ b/database/migrations/2022_05_28_135717_create_activity_logs_table.php @@ -18,7 +18,16 @@ public function up(): void $table->string('ip'); $table->text('description')->nullable(); $table->nullableNumericMorphs('actor'); - $table->json('properties'); + switch (Schema::getConnection()->getDriverName()) { + case 'mysql': + case 'mariadb': + case 'sqlite': + $table->json('properties'); + break; + case 'pgsql': + $table->jsonb('properties'); + break; + } $table->timestamp('timestamp')->useCurrent()->onUpdate(null); }); } diff --git a/database/migrations/2024_03_12_154408_remove_nests_table.php b/database/migrations/2024_03_12_154408_remove_nests_table.php index 42d1315b91..8a0419274c 100644 --- a/database/migrations/2024_03_12_154408_remove_nests_table.php +++ b/database/migrations/2024_03_12_154408_remove_nests_table.php @@ -28,10 +28,14 @@ public function up(): void } Schema::table('eggs', function (Blueprint $table) { - if (Schema::getConnection()->getDriverName() !== 'sqlite') { - $table->dropForeign('service_options_nest_id_foreign'); - } else { - $table->dropForeign(['nest_id']); + switch (Schema::getConnection()->getDriverName()) { + case 'mariadb': + case 'mysql': + $table->dropForeign('service_options_nest_id_foreign'); + break; + case 'sqlite': + $table->dropForeign(['nest_id']); + break; } $table->dropColumn('nest_id'); diff --git a/database/migrations/2024_04_21_162544_create_webhook_configurations_table.php b/database/migrations/2024_04_21_162544_create_webhook_configurations_table.php index c39805f081..001e2bc76c 100644 --- a/database/migrations/2024_04_21_162544_create_webhook_configurations_table.php +++ b/database/migrations/2024_04_21_162544_create_webhook_configurations_table.php @@ -15,7 +15,16 @@ public function up(): void $table->id(); $table->string('endpoint'); $table->string('description'); - $table->json('events'); + switch (Schema::getConnection()->getDriverName()) { + case 'mysql': + case 'mariadb': + case 'sqlite': + $table->json('events'); + break; + case 'pgsql': + $table->jsonb('events'); + break; + } $table->timestamps(); }); } diff --git a/database/migrations/2024_04_21_162552_create_webhooks_table.php b/database/migrations/2024_04_21_162552_create_webhooks_table.php index a7565e041a..181713fbca 100644 --- a/database/migrations/2024_04_21_162552_create_webhooks_table.php +++ b/database/migrations/2024_04_21_162552_create_webhooks_table.php @@ -17,7 +17,16 @@ public function up(): void $table->string('event'); $table->string('endpoint'); $table->timestamp('successful_at')->nullable(); - $table->json('payload'); + switch (Schema::getConnection()->getDriverName()) { + case 'mysql': + case 'mariadb': + case 'sqlite': + $table->json('payload'); + break; + case 'pgsql': + $table->jsonb('payload'); + break; + } $table->timestamps(); }); } diff --git a/database/migrations/2024_06_13_120409_add_oauth_column_to_users.php b/database/migrations/2024_06_13_120409_add_oauth_column_to_users.php index c5e2b3308a..63f56d0eac 100644 --- a/database/migrations/2024_06_13_120409_add_oauth_column_to_users.php +++ b/database/migrations/2024_06_13_120409_add_oauth_column_to_users.php @@ -12,7 +12,16 @@ public function up(): void { Schema::table('users', function (Blueprint $table) { - $table->json('oauth')->nullable()->after('totp_authenticated_at'); + switch (Schema::getConnection()->getDriverName()) { + case 'mysql': + case 'mariadb': + case 'sqlite': + $table->json('oauth')->nullable()->after('totp_authenticated_at'); + break; + case 'pgsql': + $table->jsonb('oauth')->nullable()->after('totp_authenticated_at'); + break; + } }); } diff --git a/database/migrations/2024_07_25_072050_convert_rules_to_array.php b/database/migrations/2024_07_25_072050_convert_rules_to_array.php index 6646a66614..b203901e72 100644 --- a/database/migrations/2024_07_25_072050_convert_rules_to_array.php +++ b/database/migrations/2024_07_25_072050_convert_rules_to_array.php @@ -16,9 +16,18 @@ public function up(): void DB::table('egg_variables')->where('id', $eggVariable->id)->update(['rules' => explode('|', $eggVariable->rules)]); }); - Schema::table('egg_variables', function (Blueprint $table) { - $table->json('rules')->change(); - }); + switch (Schema::getConnection()->getDriverName()) { + case 'sqlite': + case 'mysql': + case 'mariadb': + Schema::table('egg_variables', function (Blueprint $table) { + $table->json('rules')->change(); + }); + break; + case 'pgsql': + DB::statement('ALTER TABLE egg_variables ALTER COLUMN rules TYPE jsonb USING to_jsonb(rules)'); + break; + } } /** diff --git a/database/migrations/2024_11_04_185326_revamp_api_keys_permissions.php b/database/migrations/2024_11_04_185326_revamp_api_keys_permissions.php index ee8b649746..c57cf625d2 100644 --- a/database/migrations/2024_11_04_185326_revamp_api_keys_permissions.php +++ b/database/migrations/2024_11_04_185326_revamp_api_keys_permissions.php @@ -22,7 +22,16 @@ public function up(): void { Schema::table('api_keys', function (Blueprint $table) { - $table->json('permissions'); + switch (Schema::getConnection()->getDriverName()) { + case 'mysql': + case 'mariadb': + case 'sqlite': + $table->json('permissions'); + break; + case 'pgsql': + $table->jsonb('permissions'); + break; + } }); foreach (ApiKey::query() as $apiKey) { diff --git a/database/migrations/2025_01_08_052636_add_driver_to_database_hosts copy.php b/database/migrations/2025_01_08_052636_add_driver_to_database_hosts copy.php new file mode 100644 index 0000000000..357eb5ec37 --- /dev/null +++ b/database/migrations/2025_01_08_052636_add_driver_to_database_hosts copy.php @@ -0,0 +1,28 @@ +string('driver'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('database_hosts', function (Blueprint $table) { + $table->dropColumn('driver'); + }); + } +}; diff --git a/database/migrations/2025_01_08_111850_setup_default_postgresql_permissions.php b/database/migrations/2025_01_08_111850_setup_default_postgresql_permissions.php new file mode 100644 index 0000000000..4e9a009626 --- /dev/null +++ b/database/migrations/2025_01_08_111850_setup_default_postgresql_permissions.php @@ -0,0 +1,33 @@ +getDriverName()) { + case 'pgsql': + $database = DB::connection()->getDatabaseName(); + DB::statement(sprintf('REVOKE CONNECT ON DATABASE "%s" FROM public', $database)); + break; + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + switch (Schema::getConnection()->getDriverName()) { + case 'pgsql': + $database = DB::connection()->getDatabaseName(); + DB::statement(sprintf('GRANT CONNECT ON DATABASE "%s" TO public', $database)); + break; + } + } +};