CluPilotCloud/tests/Feature/Admin/HostTakeoverPageTest.php

181 lines
7.0 KiB
PHP

<?php
use App\Livewire\Admin\HostCreate;
use App\Models\Datacenter;
use App\Models\Host;
use App\Models\Operator;
use App\Support\HostEnrolment;
use Livewire\Livewire;
beforeEach(function () {
fakeServices();
config()->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(): \Livewire\Features\SupportTesting\Testable
{
return Livewire::actingAs(Operator::factory()->role('Owner')->create(), 'operator')
->test(HostCreate::class)
->set('name', 'pve-fsn-2')
->set('datacenter', 'fsn')
->set('public_ip', '198.51.45.9')
->set('root_password', 'ein-langes-kennwort')
->call('save');
}
/**
* Die eine Frage, die diese Seite beantworten muss: was gebe ich auf dem Server
* ein? Steht die Zeile nicht da, ist die ganze Übernahme nicht benutzbar.
*/
it('shows the command exactly once, right after creating the host', function () {
$page = createHostAs();
$page->assertSee('curl')
->assertSee('clupilot-bootstrap.sh')
->assertSee('--code');
// Ein frisch geladenes Bauteil — also jemand, der die Seite neu lädt — hat
// nichts mehr. Der Code steht nur als Hash in der Datenbank.
Livewire::actingAs(Operator::factory()->role('Owner')->create(), 'operator')
->test(HostCreate::class)
->assertDontSee('curl');
});
/**
* 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 = createHostAs()->get('command');
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(['dns_name' => 'fsn-01']);
expect(App\Support\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(createHostAs()->get('command'))
->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 = createHostAs()->get('command');
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 () {
$command = createHostAs()->get('command');
preg_match('/--code ([A-Za-z0-9]+)/', $command, $matches);
expect(HostEnrolment::claim($matches[1])?->name)->toBe('pve-fsn-2');
});
/**
* 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 dieselbe Anleitung stehen, nur mit der Zeile darin.
* Ein Betreiber, der bei Schritt 5 nicht weiterweiß, soll nicht auf einer Seite
* landen, die nur noch aus einem Kasten besteht.
*/
it('keeps the whole procedure visible after the host exists', function () {
createHostAs()
->assertSee(__('hosts.takeover.s1_title'))
->assertSee(__('hosts.takeover.s5_title'))
->assertSee(__('hosts.takeover.s6_title'))
->assertSee('--wg-private', escape: false);
});
it('refuses to create a host without the permission', function () {
Livewire::actingAs(Operator::factory()->create(), 'operator')
->test(HostCreate::class)
->set('name', 'pve-fsn-3')
->set('datacenter', 'fsn')
->set('public_ip', '198.51.45.10')
->set('root_password', 'ein-langes-kennwort')
->call('save')
->assertForbidden();
expect(Host::query()->where('name', 'pve-fsn-3')->exists())->toBeFalse();
});