host($run); if (blank($host->wg_ip)) { return StepResult::fail('The host has no management address to publish.'); } // Reserved and persisted before anything is published: releasing the // lock with only a candidate in hand let two concurrent onboardings pick // the same name and overwrite each other's record. $name = $host->dns_name ?: $this->reserveName($host); // Die PLATTFORM-Zone, nicht die Kundenzone. Diese Zeile las bis hierher // `provisioning.dns.zone` — die Zone der Kundeninstanzen —, und auf // dieser Installation hieß ein Host damit `fsn-01.node.clupilot.cloud`. // Der Kopfkommentar oben, die Prüfregel in Datacenters und der Test in // ServicesTest sagen alle drei `.com`; nur diese Zeile sagte etwas // anderes. Die zwei Zonen sind laut OfficialDomains ausdrücklich // getrennt — eine Nextcloud ist fremde Software, bei der sich Fremde // anmelden, und sie teilt sich deshalb keinen Cookie-Geltungsbereich mit // dem Portal. Ein Hostname in dieser Zone hebt die Trennung nicht auf, // aber er stellt sie in Frage, und der nächste Griff daneben ist teurer. $fqdn = $name.'.node.'.config('provisioning.dns.platform_zone'); try { $this->dns->write($name, $fqdn, $host->wg_ip); } catch (Throwable $e) { // Retried first: the write is idempotent (overwrite-by-name), so a // retry after a transient failure (e.g. the shared volume briefly // unavailable) just rewrites the same entry rather than risking a // half-written file. $allowed = min(3, (int) $run->max_attempts); if ($run->attempt + 1 < $allowed) { return StepResult::retry(30, 'DNS unavailable: '.$e->getMessage()); } // Out of attempts: a name is convenience, and a host that is // otherwise ready should not be stranded for the want of one. $run->events()->create([ 'step' => $this->key(), 'outcome' => 'info', 'message' => 'DNS name could not be registered: '.$e->getMessage(), ]); return StepResult::advance(); } return StepResult::advance(); } /** * -, numbered per datacenter and never reused. * * The counter is stored rather than derived from the hosts that happen to * exist: deriving it hands the highest number straight back out after that * host is removed, so a cached name would resolve to a different machine. */ private function reserveName(Host $host): string { // Everything keys off the LABEL, not the raw code: two legacy codes can // normalise to the same label (eu_west and eu-west), and separate // counters would then hand both of them eu-west-01 — a unique-constraint // failure that blocks onboarding rather than a DNS problem we can shrug off. $label = $this->dnsLabel($host->datacenter); return Cache::lock('host-dns-name:'.$label, 30)->block(10, function () use ($host, $label) { $key = 'dns.sequence.'.$label; $next = ((int) Settings::get($key, 0)) + 1; // Never below what is already in use — a settings store that was // wiped must not start handing out live names again. Matched on the // name itself, so it covers every code sharing this label. $inUse = Host::query() ->where('dns_name', 'like', $label.'-%') ->pluck('dns_name') ->map(fn (string $name) => (int) substr($name, strrpos($name, '-') + 1)) ->max() ?? 0; $next = max($next, $inUse + 1); $name = sprintf('%s-%02d', $label, $next); Settings::set($key, $next); $host->update(['dns_name' => $name]); // reserved while still locked return $name; }); } /** * Datacenter codes created before the rule was tightened can hold characters * a DNS label cannot; a name the provider rejects would leave the host * nameless forever. */ private function dnsLabel(string $datacenter): string { $label = trim(preg_replace('/[^a-z0-9-]+/', '-', strtolower($datacenter)) ?? '', '-'); return $label !== '' ? $label : 'node'; } }