CluPilotCloud/app/Http/Controllers/BootstrapArchiveController.php

111 lines
3.4 KiB
PHP

<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\Process\Process;
/**
* Liefert das Bootstrap-Skript als Archiv aus.
*
* Das ist der einzige Weg, auf dem das Skript auf eine nackte Maschine kommt:
* ein Rettungssystem hat nichts als curl. Es ist eine STATISCHE Datei und kein
* Endpunkt, der Auskunft gibt — Spec §5 bleibt damit unangetastet. Es steht
* nichts Vertrauliches darin; alle Geheimnisse reist der Betreiber in der
* Befehlszeile mit, nicht im Archiv.
*
* Als Archiv und nicht als eine Datei, weil `curl … | sh` keine `lib/` haben
* kann und der Skript-Plan getrennte Dateien verlangt: `network.sh` und
* `template.sh` sollen einzeln nachlesbar und wiederholbar bleiben, wenn auf
* einer Maschine um drei Uhr morgens etwas klemmt.
*
* Gebaut wird beim ersten Abruf und danach nur, wenn sich unter `deploy/bootstrap`
* etwas geändert hat. Ein Archiv, das im Repo läge, wäre die Fassung von dem
* Tag, an dem jemand zuletzt daran gedacht hat, es neu zu bauen.
*/
class BootstrapArchiveController extends Controller
{
public function __invoke(Request $request): BinaryFileResponse
{
$archive = storage_path('app/bootstrap.tar.gz');
if ($this->isStale($archive)) {
$this->build($archive);
}
return response()
->file($archive, [
'Content-Type' => 'application/gzip',
'Content-Disposition' => 'attachment; filename="bootstrap.tar.gz"',
]);
}
/**
* Neuer als alles, was drinsteckt?
*
* Über die Änderungszeit und nicht über einen Hash: das Verzeichnis ist
* klein, der Vergleich läuft bei jedem Abruf, und ein Hash über alle Dateien
* kostete bei jedem Abruf mehr als das Packen gelegentlich kostet.
*/
private function isStale(string $archive): bool
{
if (! is_file($archive)) {
return true;
}
$built = filemtime($archive);
foreach ($this->sourceFiles() as $file) {
if (filemtime($file) > $built) {
return true;
}
}
return false;
}
/** @return list<string> */
private function sourceFiles(): array
{
$root = base_path('deploy/bootstrap');
if (! is_dir($root)) {
return [];
}
$files = [];
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($root, \FilesystemIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
if ($file->isFile()) {
$files[] = $file->getPathname();
}
}
return $files;
}
/**
* Mit GNU tar und nicht mit PharData: `phar.readonly` steht in diesem
* Container auf 1, und das Ausführbar-Bit soll erhalten bleiben.
*
* Der oberste Eintrag heißt `bootstrap/`, damit die kopierte Befehlszeile
* mit `tar xz -C /opt/clupilot` genau `/opt/clupilot/bootstrap` ergibt —
* der Ort, an dem das Skript sich selbst und seine Bibliothek erwartet.
*/
private function build(string $archive): void
{
@mkdir(dirname($archive), 0755, true);
$process = new Process(
['tar', 'czf', $archive, '-C', base_path('deploy'), 'bootstrap'],
timeout: 60,
);
$process->mustRun();
}
}