Allow Database Hosts to have multiple Nodes (#767)

* WIP

* Update laravel and migrations

* WIP

* fix tests

* Update composer

* Fix transformer

* Fix filament pages

* WIP

* Update DatabaseHostTransformer

* fix: tests

* pint this files pls

* resolve merge better

* Update migration

* Update Migration, Again

* Update down migration

---------

Co-authored-by: Vehikl <go@vehikl.com>
This commit is contained in:
pelican-vehikl 2024-12-06 20:24:30 -05:00 committed by GitHub
parent 5b3ae995e6
commit 7e7f0be7df
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 95 additions and 44 deletions

View File

@ -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'),
]),
]);
}

View File

@ -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'),
]),
]);
}

View File

@ -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(),
])

View File

@ -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);
}
/**

View File

@ -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);
}
/**

View File

@ -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, [

View File

@ -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();

View File

@ -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);
}
}

View File

@ -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');

View File

@ -13,7 +13,6 @@ return new class extends Migration
{
Schema::table('servers', function (Blueprint $table) {
$table->dropForeign(['pack_id']);
$table->dropColumn('pack_id');
});
}

View File

@ -33,6 +33,7 @@ return new class extends Migration
} else {
$table->dropForeign(['nest_id']);
}
$table->dropColumn('nest_id');
});

View File

@ -0,0 +1,51 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('database_host_node', function (Blueprint $table) {
$table->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');
}
};

View File

@ -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);

View File

@ -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,