diff --git a/app/Filament/Resources/DatabaseHostResource/Pages/CreateDatabaseHost.php b/app/Filament/Resources/DatabaseHostResource/Pages/CreateDatabaseHost.php index c2078977c..f97983c89 100644 --- a/app/Filament/Resources/DatabaseHostResource/Pages/CreateDatabaseHost.php +++ b/app/Filament/Resources/DatabaseHostResource/Pages/CreateDatabaseHost.php @@ -78,12 +78,13 @@ class CreateDatabaseHost extends CreateRecord ->revealable() ->maxLength(255) ->required(), - Select::make('node_id') + Select::make('node_ids') + ->multiple() ->searchable() ->preload() ->helperText('This setting only defaults to this database host when adding a database to a server on the selected node.') - ->label('Linked Node') - ->relationship('node', 'name'), + ->label('Linked Nodes') + ->relationship('nodes', 'name'), ]), ]); } diff --git a/app/Filament/Resources/DatabaseHostResource/Pages/EditDatabaseHost.php b/app/Filament/Resources/DatabaseHostResource/Pages/EditDatabaseHost.php index c3e70c9ec..49982b71d 100644 --- a/app/Filament/Resources/DatabaseHostResource/Pages/EditDatabaseHost.php +++ b/app/Filament/Resources/DatabaseHostResource/Pages/EditDatabaseHost.php @@ -73,12 +73,13 @@ class EditDatabaseHost extends EditRecord ->password() ->revealable() ->maxLength(255), - Select::make('node_id') + Select::make('nodes') + ->multiple() ->searchable() ->preload() ->helperText('This setting only defaults to this database host when adding a database to a server on the selected node.') - ->label('Linked Node') - ->relationship('node', 'name'), + ->label('Linked Nodes') + ->relationship('nodes', 'name'), ]), ]); } diff --git a/app/Filament/Resources/DatabaseHostResource/Pages/ListDatabaseHosts.php b/app/Filament/Resources/DatabaseHostResource/Pages/ListDatabaseHosts.php index c5db9ea85..f9615f3e7 100644 --- a/app/Filament/Resources/DatabaseHostResource/Pages/ListDatabaseHosts.php +++ b/app/Filament/Resources/DatabaseHostResource/Pages/ListDatabaseHosts.php @@ -36,8 +36,9 @@ class ListDatabaseHosts extends ListRecords ->counts('databases') ->icon('tabler-database') ->label('Databases'), - TextColumn::make('node.name') + TextColumn::make('nodes.name') ->icon('tabler-server-2') + ->badge() ->placeholder('No Nodes') ->sortable(), ]) diff --git a/app/Models/DatabaseHost.php b/app/Models/DatabaseHost.php index 66e37c23e..325211a50 100644 --- a/app/Models/DatabaseHost.php +++ b/app/Models/DatabaseHost.php @@ -2,8 +2,8 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; -use Illuminate\Database\Eloquent\Relations\BelongsTo; /** * @property int $id @@ -39,7 +39,7 @@ class DatabaseHost extends Model * Fields that are mass assignable. */ protected $fillable = [ - 'name', 'host', 'port', 'username', 'password', 'max_databases', 'node_id', + 'name', 'host', 'port', 'username', 'password', 'max_databases', ]; /** @@ -51,7 +51,8 @@ class DatabaseHost extends Model 'port' => 'required|numeric|between:1,65535', 'username' => 'required|string|max:32', 'password' => 'nullable|string', - 'node_id' => 'sometimes|nullable|integer|exists:nodes,id', + 'node_ids' => 'nullable|array', + 'node_ids.*' => 'required|integer,exists:nodes,id', ]; protected function casts(): array @@ -59,7 +60,6 @@ class DatabaseHost extends Model return [ 'id' => 'integer', 'max_databases' => 'integer', - 'node_id' => 'integer', 'password' => 'encrypted', 'created_at' => 'immutable_datetime', 'updated_at' => 'immutable_datetime', @@ -71,12 +71,9 @@ class DatabaseHost extends Model return 'id'; } - /** - * Gets the node associated with a database host. - */ - public function node(): BelongsTo + public function nodes(): BelongsToMany { - return $this->belongsTo(Node::class); + return $this->belongsToMany(Node::class); } /** diff --git a/app/Models/Node.php b/app/Models/Node.php index e13f0a592..7d10006f1 100644 --- a/app/Models/Node.php +++ b/app/Models/Node.php @@ -5,6 +5,7 @@ namespace App\Models; use App\Exceptions\Service\HasActiveServersException; use App\Repositories\Daemon\DaemonConfigurationRepository; use Exception; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasManyThrough; use Illuminate\Notifications\Notifiable; @@ -243,9 +244,9 @@ class Node extends Model return $this->hasMany(Allocation::class); } - public function databaseHosts(): HasMany + public function databaseHosts(): BelongsToMany { - return $this->hasMany(DatabaseHost::class); + return $this->belongsToMany(DatabaseHost::class); } /** diff --git a/app/Services/Databases/DeployServerDatabaseService.php b/app/Services/Databases/DeployServerDatabaseService.php index 40ffedbbc..6cf60fb19 100644 --- a/app/Services/Databases/DeployServerDatabaseService.php +++ b/app/Services/Databases/DeployServerDatabaseService.php @@ -25,15 +25,15 @@ class DeployServerDatabaseService Assert::notEmpty($data['database'] ?? null); Assert::notEmpty($data['remote'] ?? null); - $hosts = DatabaseHost::query()->get()->toBase(); + $hosts = DatabaseHost::query()->get(); if ($hosts->isEmpty()) { throw new NoSuitableDatabaseHostException(); - } else { - $nodeHosts = $hosts->where('node_id', $server->node_id)->toBase(); + } - if ($nodeHosts->isEmpty() && !config('panel.client_features.databases.allow_random')) { - throw new NoSuitableDatabaseHostException(); - } + $nodeHosts = $server->node->databaseHosts()->get(); + // TODO: @areyouscared remove allow random feature for database hosts + if ($nodeHosts->isEmpty() && !config('panel.client_features.databases.allow_random')) { + throw new NoSuitableDatabaseHostException(); } return $this->managementService->create($server, [ diff --git a/app/Services/Databases/Hosts/HostCreationService.php b/app/Services/Databases/Hosts/HostCreationService.php index d4d4264f1..ff38cb5c9 100644 --- a/app/Services/Databases/Hosts/HostCreationService.php +++ b/app/Services/Databases/Hosts/HostCreationService.php @@ -33,9 +33,10 @@ class HostCreationService 'port' => array_get($data, 'port'), 'username' => array_get($data, 'username'), 'max_databases' => array_get($data, 'max_databases'), - 'node_id' => array_get($data, 'node_id'), ]); + $host->nodes()->sync(array_get($data, 'node_ids', [])); + // Confirm access using the provided credentials before saving data. $this->dynamic->set('dynamic', $host); $this->databaseManager->connection('dynamic')->getPdo(); diff --git a/app/Transformers/Api/Application/DatabaseHostTransformer.php b/app/Transformers/Api/Application/DatabaseHostTransformer.php index 0402ff7e9..8f3453f6a 100644 --- a/app/Transformers/Api/Application/DatabaseHostTransformer.php +++ b/app/Transformers/Api/Application/DatabaseHostTransformer.php @@ -5,7 +5,6 @@ namespace App\Transformers\Api\Application; use App\Models\Node; use App\Models\Database; use App\Models\DatabaseHost; -use League\Fractal\Resource\Item; use League\Fractal\Resource\Collection; use League\Fractal\Resource\NullResource; @@ -13,7 +12,7 @@ class DatabaseHostTransformer extends BaseTransformer { protected array $availableIncludes = [ 'databases', - 'node', + 'nodes', ]; /** @@ -35,7 +34,6 @@ class DatabaseHostTransformer extends BaseTransformer 'host' => $model->host, 'port' => $model->port, 'username' => $model->username, - 'node' => $model->node_id, 'created_at' => $model->created_at->toAtomString(), 'updated_at' => $model->updated_at->toAtomString(), ]; @@ -56,16 +54,16 @@ class DatabaseHostTransformer extends BaseTransformer } /** - * Include the node associated with this host. + * Include the nodes associated with this host. */ - public function includeNode(DatabaseHost $model): Item|NullResource + public function includeNodes(DatabaseHost $model): Collection|NullResource { if (!$this->authorize(Node::RESOURCE_NAME)) { return $this->null(); } - $model->loadMissing('node'); + $model->loadMissing('nodes'); - return $this->item($model->getRelation('node'), $this->makeTransformer(NodeTransformer::class), Node::RESOURCE_NAME); + return $this->collection($model->getRelation('nodes'), $this->makeTransformer(NodeTransformer::class), Node::RESOURCE_NAME); } } diff --git a/database/migrations/2017_02_09_174834_SetupPermissionsPivotTable.php b/database/migrations/2017_02_09_174834_SetupPermissionsPivotTable.php index 777889565..e1af0b0a3 100644 --- a/database/migrations/2017_02_09_174834_SetupPermissionsPivotTable.php +++ b/database/migrations/2017_02_09_174834_SetupPermissionsPivotTable.php @@ -34,6 +34,9 @@ return new class extends Migration 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']); } $table->dropColumn('server_id'); diff --git a/database/migrations/2020_09_13_110007_drop_packs_from_servers.php b/database/migrations/2020_09_13_110007_drop_packs_from_servers.php index fc87382b9..39f048b37 100644 --- a/database/migrations/2020_09_13_110007_drop_packs_from_servers.php +++ b/database/migrations/2020_09_13_110007_drop_packs_from_servers.php @@ -13,7 +13,6 @@ return new class extends Migration { Schema::table('servers', function (Blueprint $table) { $table->dropForeign(['pack_id']); - $table->dropColumn('pack_id'); }); } 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 2b5542f23..42d1315b9 100644 --- a/database/migrations/2024_03_12_154408_remove_nests_table.php +++ b/database/migrations/2024_03_12_154408_remove_nests_table.php @@ -33,6 +33,7 @@ return new class extends Migration } else { $table->dropForeign(['nest_id']); } + $table->dropColumn('nest_id'); }); diff --git a/database/migrations/2024_10_31_203540_change_database_hosts_to_belong_to_many_nodes.php b/database/migrations/2024_10_31_203540_change_database_hosts_to_belong_to_many_nodes.php new file mode 100644 index 000000000..2fc831cb1 --- /dev/null +++ b/database/migrations/2024_10_31_203540_change_database_hosts_to_belong_to_many_nodes.php @@ -0,0 +1,51 @@ +id(); + $table->unsignedInteger('node_id'); + $table->foreign('node_id')->references('id')->on('nodes'); + $table->unsignedInteger('database_host_id'); + $table->foreign('database_host_id')->references('id')->on('database_hosts'); + $table->timestamps(); + }); + + $databaseNodes = DB::table('database_hosts')->whereNotNull('node_id')->get(); + $newJoinEntries = $databaseNodes->map(fn ($record) => [ + 'node_id' => $record->node_id, + 'database_host_id' => $record->id, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + DB::table('database_host_node')->insert($newJoinEntries->toArray()); + + Schema::table('database_hosts', function (Blueprint $table) { + $table->dropForeign(['node_id']); + $table->dropColumn('node_id'); + }); + } + + public function down(): void + { + Schema::table('database_hosts', function (Blueprint $table) { + $table->unsignedInteger('node_id')->nullable(); + $table->foreign('node_id')->references('id')->on('nodes'); + }); + + foreach (DB::table('database_host_node')->get() as $record) { + DB::table('database_hosts') + ->where('id', $record->database_host_id) + ->update(['node_id' => $record->node_id]); + } + + Schema::drop('database_host_node'); + } +}; diff --git a/tests/Integration/Services/Databases/DatabaseManagementServiceTest.php b/tests/Integration/Services/Databases/DatabaseManagementServiceTest.php index 060ed9f10..834e6e345 100644 --- a/tests/Integration/Services/Databases/DatabaseManagementServiceTest.php +++ b/tests/Integration/Services/Databases/DatabaseManagementServiceTest.php @@ -52,7 +52,7 @@ class DatabaseManagementServiceTest extends IntegrationTestCase public function testDatabaseCannotBeCreatedIfServerHasReachedLimit(): void { $server = $this->createServerModel(['database_limit' => 2]); - $host = DatabaseHost::factory()->create(['node_id' => $server->node_id]); + $host = DatabaseHost::factory()->recycle($server->node)->create(); Database::factory()->times(2)->create(['server_id' => $server->id, 'database_host_id' => $host->id]); @@ -84,8 +84,8 @@ class DatabaseManagementServiceTest extends IntegrationTestCase $server = $this->createServerModel(); $name = DatabaseManagementService::generateUniqueDatabaseName('something', $server->id); - $host = DatabaseHost::factory()->create(['node_id' => $server->node_id]); - $host2 = DatabaseHost::factory()->create(['node_id' => $server->node_id]); + $host = DatabaseHost::factory()->recycle($server->node)->create(); + $host2 = DatabaseHost::factory()->recycle($server->node)->create(); Database::factory()->create([ 'database' => $name, 'database_host_id' => $host->id, @@ -117,7 +117,7 @@ class DatabaseManagementServiceTest extends IntegrationTestCase $server = $this->createServerModel(); $name = DatabaseManagementService::generateUniqueDatabaseName('something', $server->id); - $host = DatabaseHost::factory()->create(['node_id' => $server->node_id]); + $host = DatabaseHost::factory()->recycle($server->node)->create(); $username = null; $secondUsername = null; @@ -154,7 +154,7 @@ class DatabaseManagementServiceTest extends IntegrationTestCase $server = $this->createServerModel(); $name = DatabaseManagementService::generateUniqueDatabaseName('something', $server->id); - $host = DatabaseHost::factory()->create(['node_id' => $server->node_id]); + $host = DatabaseHost::factory()->recycle($server->node)->create(); $this->repository->expects('createDatabase')->with($name)->andThrows(new \BadMethodCallException()); $this->repository->expects('dropDatabase')->with($name); diff --git a/tests/Integration/Services/Databases/DeployServerDatabaseServiceTest.php b/tests/Integration/Services/Databases/DeployServerDatabaseServiceTest.php index 062dbe07d..60986f576 100644 --- a/tests/Integration/Services/Databases/DeployServerDatabaseServiceTest.php +++ b/tests/Integration/Services/Databases/DeployServerDatabaseServiceTest.php @@ -62,7 +62,7 @@ class DeployServerDatabaseServiceTest extends IntegrationTestCase $server = $this->createServerModel(); $node = Node::factory()->create(); - DatabaseHost::factory()->create(['node_id' => $node->id]); + DatabaseHost::factory()->recycle($node)->create(); config()->set('panel.client_features.databases.allow_random', false); @@ -95,10 +95,7 @@ class DeployServerDatabaseServiceTest extends IntegrationTestCase public function testDatabaseHostOnSameNodeIsPreferred(): void { $server = $this->createServerModel(); - - $node = Node::factory()->create(); - DatabaseHost::factory()->create(['node_id' => $node->id]); - $host = DatabaseHost::factory()->create(['node_id' => $server->node_id]); + $host = DatabaseHost::factory()->recycle($server->node)->create(); $this->managementService->expects('create')->with($server, [ 'database_host_id' => $host->id, @@ -124,7 +121,7 @@ class DeployServerDatabaseServiceTest extends IntegrationTestCase $server = $this->createServerModel(); $node = Node::factory()->create(); - $host = DatabaseHost::factory()->create(['node_id' => $node->id]); + $host = DatabaseHost::factory()->recycle($node)->create(); $this->managementService->expects('create')->with($server, [ 'database_host_id' => $host->id,