fix(datacenters): a code becomes a DNS label, so validate it as one
alpha_dash accepted eu_west, -edge and edge-, none of which are valid DNS labels — every host in such a datacenter would have failed registration and, because DNS is non-fatal, silently ended up without a name. The console now requires a proper label, and the step normalises anything created before that rule so existing rows still get a usable name. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>feat/portal-design
parent
69043f9ffc
commit
e96f4c1c1a
|
|
@ -11,7 +11,7 @@ use Livewire\Component;
|
||||||
#[Layout('layouts.admin')]
|
#[Layout('layouts.admin')]
|
||||||
class Datacenters extends Component
|
class Datacenters extends Component
|
||||||
{
|
{
|
||||||
#[Validate('required|string|max:8|alpha_dash|unique:datacenters,code')]
|
#[Validate(['required', 'string', 'max:8', 'regex:/^[a-z0-9]([a-z0-9-]{0,6}[a-z0-9])?$/', 'unique:datacenters,code'])]
|
||||||
public string $code = '';
|
public string $code = '';
|
||||||
|
|
||||||
#[Validate('required|string|max:255')]
|
#[Validate('required|string|max:255')]
|
||||||
|
|
@ -31,7 +31,10 @@ class Datacenters extends Component
|
||||||
// Country must be one of the curated list — a forged request can bypass
|
// Country must be one of the curated list — a forged request can bypass
|
||||||
// the <select>, so enforce membership server-side (mirrors EditDatacenter).
|
// the <select>, so enforce membership server-side (mirrors EditDatacenter).
|
||||||
$data = $this->validate([
|
$data = $this->validate([
|
||||||
'code' => 'required|string|max:8|alpha_dash|unique:datacenters,code',
|
// A datacenter code becomes a DNS label (fsn-01.node.clupilot.com),
|
||||||
|
// so it has to be one: lowercase, no underscores, no leading or
|
||||||
|
// trailing dash.
|
||||||
|
'code' => ['required', 'string', 'max:8', 'regex:/^[a-z0-9]([a-z0-9-]{0,6}[a-z0-9])?$/', 'unique:datacenters,code'],
|
||||||
'name' => 'required|string|max:255',
|
'name' => 'required|string|max:255',
|
||||||
'location' => ['nullable', Rule::in(array_keys((array) config('countries')))],
|
'location' => ['nullable', Rule::in(array_keys((array) config('countries')))],
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,11 @@ class RegisterHostDns extends HostStep
|
||||||
->max() ?? 0;
|
->max() ?? 0;
|
||||||
$next = max($next, $inUse + 1);
|
$next = max($next, $inUse + 1);
|
||||||
|
|
||||||
$name = sprintf('%s-%02d', $host->datacenter, $next);
|
// Datacenter codes created before the rule was tightened can still
|
||||||
|
// hold characters a DNS label cannot; a name that the provider
|
||||||
|
// rejects would leave the host nameless forever.
|
||||||
|
$label = trim(preg_replace('/[^a-z0-9-]+/', '-', strtolower($host->datacenter)) ?? '', '-');
|
||||||
|
$name = sprintf('%s-%02d', $label !== '' ? $label : 'node', $next);
|
||||||
|
|
||||||
Settings::set($key, $next);
|
Settings::set($key, $next);
|
||||||
$host->update(['dns_name' => $name]); // reserved while still locked
|
$host->update(['dns_name' => $name]); // reserved while still locked
|
||||||
|
|
|
||||||
|
|
@ -344,3 +344,20 @@ it('keeps the host until its DNS record is really gone', function () {
|
||||||
expect(App\Models\Host::query()->whereKey($host->id)->exists())->toBeFalse()
|
expect(App\Models\Host::query()->whereKey($host->id)->exists())->toBeFalse()
|
||||||
->and($s['dns']->records)->toBe([]);
|
->and($s['dns']->records)->toBe([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('builds a valid DNS label even from an awkward datacenter code', function () {
|
||||||
|
$s = fakeServices();
|
||||||
|
// Inserted directly: the console now rejects this shape, but rows created
|
||||||
|
// before the rule was tightened still exist.
|
||||||
|
App\Models\Datacenter::factory()->create(['code' => 'eu_west', 'name' => 'EU West']);
|
||||||
|
$host = App\Models\Host::factory()->active()->create([
|
||||||
|
'datacenter' => 'eu_west', 'wg_ip' => '10.66.0.70', 'dns_name' => null,
|
||||||
|
]);
|
||||||
|
$run = App\Models\ProvisioningRun::factory()->forHost($host)->create(['pipeline' => 'host']);
|
||||||
|
|
||||||
|
(new App\Provisioning\Steps\Host\RegisterHostDns($s['dns']))->execute($run);
|
||||||
|
|
||||||
|
$name = $host->fresh()->dns_name;
|
||||||
|
expect($name)->toBe('eu-west-01')
|
||||||
|
->and($name)->toMatch('/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/');
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue