fix(engine): one host per public IP (unique constraint + validation)

Prevents two onboarding runs from fighting over the same physical server.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat/portal-design
nexxo 2026-07-25 10:43:30 +02:00
parent 48ed5e7d34
commit 91e308efb0
4 changed files with 39 additions and 2 deletions

View File

@ -16,7 +16,7 @@ class HostCreate extends Component
#[Validate('required|string|in:fsn,hel')] #[Validate('required|string|in:fsn,hel')]
public string $datacenter = 'fsn'; public string $datacenter = 'fsn';
#[Validate('required|ip')] #[Validate('required|ip|unique:hosts,public_ip')]
public string $public_ip = ''; public string $public_ip = '';
#[Validate('required|string|min:8')] #[Validate('required|string|min:8')]

View File

@ -15,7 +15,7 @@ class HostFactory extends Factory
return [ return [
'name' => 'pve-test-'.$this->faker->unique()->numberBetween(1, 99999), 'name' => 'pve-test-'.$this->faker->unique()->numberBetween(1, 99999),
'datacenter' => 'fsn', 'datacenter' => 'fsn',
'public_ip' => $this->faker->ipv4(), 'public_ip' => $this->faker->unique()->ipv4(),
'status' => 'pending', 'status' => 'pending',
]; ];
} }

View File

@ -0,0 +1,24 @@
<?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
{
// One physical server = one host record: prevents two onboarding runs
// from fighting over the same machine's SSH/WireGuard/token config.
Schema::table('hosts', function (Blueprint $table) {
$table->unique('public_ip');
});
}
public function down(): void
{
Schema::table('hosts', function (Blueprint $table) {
$table->dropUnique(['public_ip']);
});
}
};

View File

@ -58,6 +58,19 @@ it('creates a host and starts onboarding with an encrypted password', function (
Queue::assertPushed(AdvanceRunJob::class); Queue::assertPushed(AdvanceRunJob::class);
}); });
it('rejects a duplicate public ip', function () {
Host::factory()->create(['public_ip' => '203.0.113.99']);
Livewire::actingAs(admin())
->test(HostCreate::class)
->set('name', 'pve-dup')
->set('datacenter', 'fsn')
->set('public_ip', '203.0.113.99')
->set('root_password', 'supersecret')
->call('save')
->assertHasErrors(['public_ip']);
});
it('validates the add-host form', function () { it('validates the add-host form', function () {
Livewire::actingAs(admin()) Livewire::actingAs(admin())
->test(HostCreate::class) ->test(HostCreate::class)