111 lines
3.4 KiB
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();
|
|
}
|
|
}
|