set('provisioning.wireguard.hub_public_key', 'HUBKEYHUBKEYHUBKEYHUBKEYHUBKEYHUBKEYHUBKEY0='); config()->set('provisioning.wireguard.endpoint', 'hub.clupilot.cloud:51820'); config()->set('provisioning.wireguard.hub_address', '10.66.0.1'); config()->set('provisioning.wireguard.subnet', '10.66.0.0/24'); config()->set('admin_access.app_host', 'app.clupilot.com'); config()->set('provisioning.dns.platform_zone', 'clupilot.com'); // firstOrCreate: die Migrationen legen Rechenzentren schon an, und ein // zweites `fsn` verletzt den eindeutigen Index. Datacenter::query()->firstOrCreate( ['code' => 'fsn'], ['name' => 'Falkenstein', 'location' => 'DE', 'active' => true], ); }); function createHostAs(): Testable { return Livewire::actingAs(Operator::factory()->role('Owner')->create(), 'operator') ->test(HostCreate::class) ->set('datacenter', 'fsn') ->set('public_ip', '198.51.45.9') ->set('root_password', 'ein-langes-kennwort') ->call('save'); } /** * Die Bootstrap-Zeile, wie sie der Weg über das Rettungssystem braucht. * * Sie wird hier direkt gebaut statt über eine Seite abgegriffen. Ihre * Richtigkeit hat nichts damit zu tun, wer sie anzeigt — und genau diese * Kopplung war der Grund, warum fünf Tests umfielen, als die Anlegen-Seite * aufhörte, sie zu zeigen. */ function takeoverCommandFor(Host $host): string { return HostTakeoverCommand::for($host, HostEnrolment::issueWithKeys($host)); } /** * Die Anlegen-Seite zeigt keine Befehlszeile mehr. * * Sie gehörte zum Bootstrap-Weg: Rettungssystem starten, Zeile kopieren, dort * einfügen. Das Speichern startet aber die Provisionierungs-Pipeline, und die * meldet sich mit dem Root-Passwort selbst an einer laufenden Debian-Maschine * an. Es gibt nichts zu kopieren — die Seite behauptete es trotzdem. */ it('does not hand the operator a command line they do not need', function () { createHostAs() ->assertDontSee('clupilot-bootstrap.sh') ->assertDontSee('--code') // Stattdessen der Weg zu dem, was jetzt wirklich passiert. ->assertSee(__('hosts.takeover.watch')); }); /** * Der Weg über das Rettungssystem bleibt erreichbar — nur nicht mehr von der * Anlegen-Seite aus. Wer leeres Blech ohne vorinstalliertes System hat, * bekommt die Zeile über Admin\ReissueTakeover. */ it('still issues the command line where the rescue route lives', function () { $host = Host::factory()->create(['datacenter' => 'fsn']); Livewire::actingAs(Operator::factory()->role('Owner')->create(), 'operator') ->test(ReissueTakeover::class, ['uuid' => $host->uuid]) ->call('issue') ->assertSee('clupilot-bootstrap.sh'); }); /** * Die Zeile muss ALLES tragen, was das Skript vor dem Tunnel braucht (Spec §5). * Fehlt ein Wert, läuft sie an und endet in einem Tunnel ohne Handshake — und * das merkt man erst auf einer Maschine, die schon bestellt ist. */ it('carries every value the script needs before the tunnel', function () { $command = takeoverCommandFor(Host::factory()->create(['datacenter' => 'fsn'])); foreach (['--code', '--wg-private', '--wg-ip', '--hub-pubkey', '--hub-endpoint', '--api', '--fqdn'] as $argument) { expect($command)->toContain($argument); } expect($command) ->toContain('hub.clupilot.cloud:51820') ->toContain('--api http://10.66.0.1') ->toContain('/opt/clupilot'); // Der Name des Hosts liegt auf der PLATTFORM-Zone. `.cloud` gehört den // Kundeninstanzen und ist laut OfficialDomains bewusst eine andere // registrierbare Domain — geprüft wird deshalb der --fqdn-Wert selbst und // nicht die ganze Zeile: der Hub-Endpunkt ist eine eigene Einstellung und // darf heißen, wie er will. preg_match('/--fqdn (\S+)/', $command, $fqdn); expect($fqdn[1] ?? '')->toEndWith('.node.clupilot.com'); }); /** * Dieselbe Trennung an der Stelle, an der sie schon einmal gebrochen war. * * `RegisterHostDns` nennt im eigenen Kopfkommentar `fsn-01.node.clupilot.com`, * die Prüfregel in `Datacenters` nennt denselben Namen, und `ServicesTest` * schreibt ihn wörtlich — der Schritt baute den Namen aber aus * `provisioning.dns.zone`, und das ist die KUNDENZONE. Auf dieser Installation * hieß ein Host damit `fsn-01.node.clupilot.cloud`. */ it('keeps host names out of the customer zone', function () { config()->set('provisioning.dns.zone', 'clupilot.cloud'); config()->set('provisioning.dns.platform_zone', 'clupilot.com'); $host = Host::factory()->create(['name' => 'fsn-01']); expect(HostTakeoverCommand::fqdnFor($host)) ->toBe('fsn-01.node.clupilot.com'); }); /** * Über den ÖFFENTLICHEN Hostnamen. Die Konsole läuft unter admin.…, aber die * Zeile wird auf einer Maschine ausgeführt, die den Adminbereich nicht erreichen * darf — sie ist genau dafür abgeriegelt. Ein Link auf den Konsolen-Hostnamen * liefe in eine 404 auf einem Server, an den man dann nur noch über die * Anbieterkonsole kommt. */ it('fetches the installer over the public hostname', function () { // Vom Datei-Hostnamen, seit es einen gibt — und in keinem Fall vom // Konsolen-Hostnamen: die Konsole ist per Client-IP abgeriegelt // (RestrictConsoleNetwork), und ein frischer Server im Rettungssystem steht // in keinem der freigegebenen Bereiche. expect(takeoverCommandFor(Host::factory()->create(['datacenter' => 'fsn']))) ->toContain('https://files.clupilot.test/bootstrap.tar.gz') ->not->toContain('admin.'); }); /** * Der Schlüssel ist base64 und enthält +, / und =. Ohne Anführungszeichen * zerlegt die Shell die Zeile an einer Stelle, die niemand sieht. */ it('quotes the keys so a shell cannot split them', function () { $command = takeoverCommandFor(Host::factory()->create(['datacenter' => 'fsn'])); expect($command)->toMatch("/--wg-private '[^']+'/") ->and($command)->toMatch("/--hub-pubkey '[^']+'/"); }); /** * Der Code aus der Zeile muss der sein, mit dem der Host sich meldet. Sonst ist * die Anleitung richtig und der Ablauf trotzdem kaputt. */ it('shows a code that actually resolves to the new host', function () { $host = Host::factory()->create(['datacenter' => 'fsn', 'name' => 'fsn-07']); $command = takeoverCommandFor($host); preg_match('/--code ([A-Za-z0-9]+)/', $command, $matches); // Auf DIESEN Host, nicht bloß auf irgendeinen: der Code ist der Ausweis, // mit dem sich der Server meldet. expect(HostEnrolment::claim($matches[1])?->is($host))->toBeTrue(); }); /** * Ein leerer Hub-Schlüssel ergibt eine Zeile, die läuft und nie handshaked. * Das gehört gesagt, BEVOR jemand einen Server bestellt. */ it('warns before creating a host when the tunnel settings are missing', function () { config()->set('provisioning.wireguard.hub_public_key', ''); config()->set('provisioning.wireguard.endpoint', ''); Livewire::actingAs(Operator::factory()->role('Owner')->create(), 'operator') ->test(HostCreate::class) ->assertSee('CLUPILOT_WG_HUB_PUBKEY') ->assertSee('CLUPILOT_WG_ENDPOINT'); }); /** * Nach dem Anlegen bleibt derselbe Ablauf stehen, nur einen Schritt weiter. * Die Schiene bleibt vollstaendig sichtbar: wer wissen will, was noch kommt, * soll nicht auf einer Seite landen, die nur noch aus einem Knopf besteht. */ it('keeps the whole procedure visible after the host exists', function () { createHostAs() ->assertSee(__('hosts.takeover.s1_label')) ->assertSee(__('hosts.takeover.s2_label')) ->assertSee(__('hosts.takeover.s3_label')); }); it('refuses to create a host without the permission', function () { Livewire::actingAs(Operator::factory()->create(), 'operator') ->test(HostCreate::class) ->set('datacenter', 'fsn') ->set('public_ip', '198.51.45.10') ->set('root_password', 'ein-langes-kennwort') ->call('save') ->assertForbidden(); expect(Host::query()->where('public_ip', '198.51.45.10')->exists())->toBeFalse(); });