feat(updateservice): Plattform-Dimension, signierte Releases, Update mit Rollback
Behebt eine Reihe zusammenhaengender Fehler im Update-Weg, die zusammen verhindert haben, fuer mehr als eine Plattform auszuliefern - und die im Fehlerfall halb aktualisierte Installationen hinterliessen. Server - Migration 009: Spalte platform samt neuem Unique-Key. Zuvor verdraengte das zuletzt veroeffentlichte Paket alle anderen Plattformen derselben Version, weil ON DUPLICATE KEY auf (slug, version, channel) griff. Ein Linux-System zog sich damit das Windows-Paket. - Aufloesungsregel: je Version das plattformgenaue Paket, sonst das plattformunabhaengige. Ein Client ohne Plattformangabe sieht ausschliesslich 'any' - lieber kein Update als das falsche. - manifest_json wird endlich befuellt; die Spalte blieb bisher immer leer, wodurch die API nie der Rueckfall sein konnte, als der sie gedacht war. - Releases werden serverseitig mit RSA-SHA256 signiert, neuer Endpunkt /api/updateservice/v1/pubkey. Bewusst kein HMAC: der Pruefende laeuft auf fremden Systemen und darf den Signierschluessel nicht besitzen. Packager - Bricht ab, statt die Versionshistorie zu verlieren. Schlug das Lesen der bestehenden latest.json fehl, ersetzte ein leeres catch die komplette Historie durch einen einzigen Eintrag - ohne jede Meldung. - Echte Glob-Muster. Zuvor trafen "logs/**" und "scratch/**" aus der mitgelieferten Beispielkonfiguration nie zu. - preservePatterns: Konfigurationsvorlagen werden ausgeliefert, ersetzen am Ziel aber keine vorhandene Datei. Eine settings.json mit Zugangsdaten ueberschrieb bisher beim Update die Konfiguration jedes Zielsystems. - Warnt vor Dateien, die nach Zugangsdaten aussehen und auf keiner Liste stehen. - Prueft --version gegen die Hauptassembly. Eine Abweichung fuehrte zu einer Endlosschleife: Clients aktualisieren, melden weiter die alte Version, halten das Release erneut fuer neu. - --platform mit Ableitung aus dem Publish-Pfad. Agent - Anwenden mit Plan, Backup und vollstaendigem Rollback. Die Stelle war als "Atomic Replace with Backup" kommentiert und war eine Kopierschleife. - Verwaiste Dateien werden entfernt, aber nur solche aus dem Manifest der Vorversion. Was nicht aus einem Release stammt, bleibt liegen. - Das laufende Agent-Binary wird zur Seite gelegt statt ueberschrieben. - API-Rueckfall in FetchManifestAsync; bisher nur im SDK vorhanden, weshalb die Anwendung "Update verfuegbar" und der Agent "kein Release" sagen konnte. - Installierte Version aus --current-version oder manifest.json statt des Textes "Unbekannt", der als 0 gelesen wurde und jede Version neuer erscheinen liess. Reparatur funktioniert damit auch ohne manifest.json. - Setzt das Ausfuehrungsbit fuer Linux-Pakete, die unter Windows gebaut wurden. SDK - ResolveAgentPath() liefert den plattformrichtigen Namen; ein fest verdrahtetes "update-agent.exe" wird unter Linux nie gefunden. - LaunchUpdateAgent uebergibt jetzt --restart (wurde nie uebergeben, die Anwendung blieb nach dem Update zu), --wait-for-pid (kein Wettlauf mehr mit dem Herunterfahren) und --platform. Enthaelt ausserdem die bislang nicht committete Arbeit an Watchdog, Lizenz- Client und cli/tick.php samt Migration 008; die betroffenen Dateien liessen sich nicht getrennt stagen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
5f9b0c5596
commit
2388b5abe1
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Deploymentcenter\Modules\UpdateService;
|
||||
|
||||
use Deploymentcenter\Core\Config;
|
||||
use Deploymentcenter\Core\Logger;
|
||||
|
||||
/**
|
||||
* Signiert Releases, damit ein Agent pruefen kann, dass Paket und Metadaten
|
||||
* wirklich aus diesem Deploymentcenter stammen.
|
||||
*
|
||||
* Warum asymmetrisch und nicht HMAC wie beim Lizenzmodul: Beim Lizenzmodul
|
||||
* prueft der Server, der Schluessel bleibt also hier. Ein Update wird dagegen
|
||||
* auf dem Zielsystem geprueft. Ein dort hinterlegter HMAC-Schluessel liesse
|
||||
* sich auslesen und zum Signieren beliebiger Pakete verwenden - die Signatur
|
||||
* wuerde damit genau die Eigenschaft verlieren, wegen der es sie gibt.
|
||||
*
|
||||
* Signiert wird eine kanonische Zeile, nicht das Manifest-JSON: JSON-Ausgabe
|
||||
* ist nicht bytestabil (Schluesselreihenfolge, Escaping, Zahlenformat), eine
|
||||
* Signatur darueber waere unzuverlaessig pruefbar.
|
||||
*
|
||||
* Ohne hinterlegten Schluessel ist Signieren abgeschaltet. Releases bleiben
|
||||
* dann unsigniert und installierbar; der Agent weist darauf hin.
|
||||
*/
|
||||
final class ReleaseSigner
|
||||
{
|
||||
/**
|
||||
* Kanonische Darstellung eines Releases.
|
||||
*
|
||||
* Reihenfolge und Trennzeichen sind Teil des Formats und duerfen sich
|
||||
* nicht aendern, ohne dass alle bestehenden Signaturen ungueltig werden.
|
||||
* Der Paket-Hash ist der eigentlich zu schuetzende Wert - alles andere
|
||||
* bindet ihn an genau ein Produkt, eine Version und eine Plattform.
|
||||
*/
|
||||
public static function canonical(
|
||||
string $productSlug,
|
||||
string $version,
|
||||
string $channel,
|
||||
string $platform,
|
||||
?string $sha256Hash,
|
||||
string $downloadUrl,
|
||||
int $sizeBytes
|
||||
): string {
|
||||
return implode("\n", [
|
||||
'dc-release-v1',
|
||||
$productSlug,
|
||||
$version,
|
||||
$channel,
|
||||
$platform,
|
||||
strtolower((string)$sha256Hash),
|
||||
$downloadUrl,
|
||||
(string)$sizeBytes,
|
||||
]);
|
||||
}
|
||||
|
||||
/** Ist ein Signierschluessel hinterlegt? */
|
||||
public static function isEnabled(): bool
|
||||
{
|
||||
return self::privateKeyPem() !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Signiert die kanonische Zeile. Rueckgabe base64, oder null wenn kein
|
||||
* Schluessel hinterlegt ist oder das Signieren fehlschlaegt.
|
||||
*/
|
||||
public static function sign(string $canonical): ?string
|
||||
{
|
||||
$pem = self::privateKeyPem();
|
||||
if ($pem === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!function_exists('openssl_sign')) {
|
||||
Logger::warning('Release-Signatur nicht moeglich: OpenSSL-Erweiterung fehlt.');
|
||||
return null;
|
||||
}
|
||||
|
||||
$key = openssl_pkey_get_private($pem);
|
||||
if ($key === false) {
|
||||
Logger::warning('Release-Signatur nicht moeglich: privater Schluessel nicht lesbar.');
|
||||
return null;
|
||||
}
|
||||
|
||||
$signature = '';
|
||||
$ok = openssl_sign($canonical, $signature, $key, OPENSSL_ALGO_SHA256);
|
||||
|
||||
if ($ok !== true || $signature === '') {
|
||||
Logger::warning('Release-Signatur fehlgeschlagen.');
|
||||
return null;
|
||||
}
|
||||
|
||||
return base64_encode($signature);
|
||||
}
|
||||
|
||||
/**
|
||||
* Oeffentlicher Schluessel im PEM-Format, aus dem privaten abgeleitet.
|
||||
* Er wird ueber /api/updateservice/v1/pubkey ausgeliefert.
|
||||
*/
|
||||
public static function publicKeyPem(): ?string
|
||||
{
|
||||
$pem = self::privateKeyPem();
|
||||
if ($pem === '' || !function_exists('openssl_pkey_get_private')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$key = openssl_pkey_get_private($pem);
|
||||
if ($key === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$details = openssl_pkey_get_details($key);
|
||||
if (!is_array($details) || !isset($details['key']) || !is_string($details['key'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $details['key'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fingerabdruck des oeffentlichen Schluessels (SHA256 ueber das PEM).
|
||||
* Damit kann ein Agent den einmal geholten Schluessel wiedererkennen,
|
||||
* statt ihm bei jedem Aufruf neu zu vertrauen.
|
||||
*/
|
||||
public static function publicKeyFingerprint(): ?string
|
||||
{
|
||||
$pub = self::publicKeyPem();
|
||||
return $pub === null ? null : hash('sha256', $pub);
|
||||
}
|
||||
|
||||
/**
|
||||
* Der Schluessel darf als PEM direkt oder als Pfad zu einer Datei
|
||||
* hinterlegt sein. Ein Pfad ist auf Servern meist die sauberere Ablage,
|
||||
* weil der Schluessel dann nicht in der Konfiguration steht.
|
||||
*/
|
||||
private static function privateKeyPem(): string
|
||||
{
|
||||
$value = trim((string)Config::get('security.release_private_key', ''));
|
||||
|
||||
if ($value === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (str_contains($value, '-----BEGIN')) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (is_file($value) && is_readable($value)) {
|
||||
$contents = file_get_contents($value);
|
||||
return $contents === false ? '' : trim($contents);
|
||||
}
|
||||
|
||||
Logger::warning('security.release_private_key zeigt weder auf ein PEM noch auf eine lesbare Datei.');
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,9 @@ use PDO;
|
||||
*/
|
||||
final class UpdateManager
|
||||
{
|
||||
/** Kennung fuer plattformunabhaengige Releases. */
|
||||
public const PLATFORM_ANY = 'any';
|
||||
|
||||
private PDO $db;
|
||||
|
||||
public function __construct(PDO $db)
|
||||
@@ -25,19 +28,104 @@ final class UpdateManager
|
||||
$this->db = $db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bringt eine Laufzeitkennung auf eine gueltige Form.
|
||||
*
|
||||
* Erwartet wird eine .NET-RID wie win-x64 oder linux-musl-arm64. Alles
|
||||
* Leere wird zu 'any'; unbrauchbare Angaben ebenfalls, damit ein
|
||||
* verunglueckter Parameter nicht dazu fuehrt, dass gar kein Release mehr
|
||||
* gefunden wird.
|
||||
*/
|
||||
public static function normalizePlatform(?string $platform): string
|
||||
{
|
||||
$value = strtolower(trim((string)$platform));
|
||||
|
||||
if ($value === '' || preg_match('/^[a-z0-9][a-z0-9._-]{0,31}$/', $value) !== 1) {
|
||||
return self::PLATFORM_ANY;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases eines Kanals, gefiltert auf die passende Plattform.
|
||||
*
|
||||
* Die Regel lautet: je Version gewinnt das Paket der angefragten
|
||||
* Plattform, sonst das plattformunabhaengige. Damit bleiben Releases von
|
||||
* vor der Plattform-Migration ('any') fuer alle Clients erreichbar,
|
||||
* waehrend ein Client mit Plattformangabe niemals das Paket einer fremden
|
||||
* Plattform bekommt.
|
||||
*
|
||||
* Ein Client ohne Plattformangabe sieht ausschliesslich 'any'. Das ist
|
||||
* bewusst streng: lieber kein Update als das Windows-Paket auf einem
|
||||
* Linux-System.
|
||||
*
|
||||
* @return list<array<string,mixed>>
|
||||
*/
|
||||
private function releasesFor(string $productSlug, string $channel, ?string $platform): array
|
||||
{
|
||||
$requested = self::normalizePlatform($platform);
|
||||
|
||||
$candidates = $requested === self::PLATFORM_ANY
|
||||
? [self::PLATFORM_ANY]
|
||||
: [$requested, self::PLATFORM_ANY];
|
||||
|
||||
$placeholders = implode(', ', array_map(
|
||||
static fn(int $i): string => ':platform' . $i,
|
||||
array_keys($candidates)
|
||||
));
|
||||
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT * FROM updateservice_releases
|
||||
WHERE product_slug = :slug AND channel = :channel
|
||||
AND platform IN (' . $placeholders . ')
|
||||
');
|
||||
|
||||
$params = [':slug' => $productSlug, ':channel' => $channel];
|
||||
foreach ($candidates as $i => $candidate) {
|
||||
$params[':platform' . $i] = $candidate;
|
||||
}
|
||||
$stmt->execute($params);
|
||||
|
||||
$rows = $stmt->fetchAll() ?: [];
|
||||
|
||||
// Je Version nur einen Datensatz behalten - den plattformgenauen.
|
||||
$byVersion = [];
|
||||
foreach ($rows as $row) {
|
||||
$version = (string)($row['version'] ?? '');
|
||||
if ($version === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rowPlatform = (string)($row['platform'] ?? self::PLATFORM_ANY);
|
||||
$existing = $byVersion[$version] ?? null;
|
||||
|
||||
if ($existing === null) {
|
||||
$byVersion[$version] = $row;
|
||||
continue;
|
||||
}
|
||||
|
||||
$existingPlatform = (string)($existing['platform'] ?? self::PLATFORM_ANY);
|
||||
if ($existingPlatform === self::PLATFORM_ANY && $rowPlatform !== self::PLATFORM_ANY) {
|
||||
$byVersion[$version] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($byVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ermittelt das neueste Release, das echt neuer ist als die uebergebene Version.
|
||||
*
|
||||
* @return array<string,mixed>|null
|
||||
*/
|
||||
public function checkUpdate(string $productSlug, string $currentVersion, string $channel = 'prod'): ?array
|
||||
{
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT * FROM updateservice_releases
|
||||
WHERE product_slug = :slug AND channel = :channel
|
||||
');
|
||||
$stmt->execute([':slug' => $productSlug, ':channel' => $channel]);
|
||||
$releases = $stmt->fetchAll() ?: [];
|
||||
public function checkUpdate(
|
||||
string $productSlug,
|
||||
string $currentVersion,
|
||||
string $channel = 'prod',
|
||||
?string $platform = null
|
||||
): ?array {
|
||||
$releases = $this->releasesFor($productSlug, $channel, $platform);
|
||||
|
||||
if ($releases === []) {
|
||||
return null;
|
||||
@@ -52,14 +140,9 @@ final class UpdateManager
|
||||
}
|
||||
|
||||
/** Hoechstes Release eines Kanals, unabhaengig von der Client-Version. */
|
||||
public function latestRelease(string $productSlug, string $channel = 'prod'): ?array
|
||||
public function latestRelease(string $productSlug, string $channel = 'prod', ?string $platform = null): ?array
|
||||
{
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT * FROM updateservice_releases
|
||||
WHERE product_slug = :slug AND channel = :channel
|
||||
');
|
||||
$stmt->execute([':slug' => $productSlug, ':channel' => $channel]);
|
||||
return Version::highest($stmt->fetchAll() ?: []);
|
||||
return Version::highest($this->releasesFor($productSlug, $channel, $platform));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,41 +161,50 @@ final class UpdateManager
|
||||
int $sizeBytes = 0,
|
||||
?string $manifestJson = null,
|
||||
bool $isCritical = false,
|
||||
string $author = 'admin'
|
||||
string $author = 'admin',
|
||||
?string $platform = null,
|
||||
?string $manifestSignature = null
|
||||
): array {
|
||||
$existing = $this->findRelease($productSlug, $version, $channel);
|
||||
$platform = self::normalizePlatform($platform);
|
||||
|
||||
$existing = $this->findRelease($productSlug, $version, $channel, $platform);
|
||||
|
||||
$stmt = $this->db->prepare('
|
||||
INSERT INTO updateservice_releases (
|
||||
product_slug, version, channel, release_notes, download_url,
|
||||
sha256_hash, git_commit, size_bytes, manifest_json, is_critical
|
||||
product_slug, version, channel, platform, release_notes, download_url,
|
||||
sha256_hash, git_commit, size_bytes, manifest_json, manifest_signature,
|
||||
is_critical
|
||||
) VALUES (
|
||||
:slug, :version, :channel, :notes, :url,
|
||||
:hash, :git, :size, :manifest, :critical
|
||||
:slug, :version, :channel, :platform, :notes, :url,
|
||||
:hash, :git, :size, :manifest, :signature,
|
||||
:critical
|
||||
) ON DUPLICATE KEY UPDATE
|
||||
release_notes = VALUES(release_notes),
|
||||
download_url = VALUES(download_url),
|
||||
sha256_hash = VALUES(sha256_hash),
|
||||
git_commit = VALUES(git_commit),
|
||||
size_bytes = VALUES(size_bytes),
|
||||
manifest_json = VALUES(manifest_json),
|
||||
is_critical = VALUES(is_critical)
|
||||
release_notes = VALUES(release_notes),
|
||||
download_url = VALUES(download_url),
|
||||
sha256_hash = VALUES(sha256_hash),
|
||||
git_commit = VALUES(git_commit),
|
||||
size_bytes = VALUES(size_bytes),
|
||||
manifest_json = VALUES(manifest_json),
|
||||
manifest_signature = VALUES(manifest_signature),
|
||||
is_critical = VALUES(is_critical)
|
||||
');
|
||||
|
||||
$stmt->execute([
|
||||
':slug' => $productSlug,
|
||||
':version' => $version,
|
||||
':channel' => $channel,
|
||||
':notes' => $releaseNotes,
|
||||
':url' => $downloadUrl,
|
||||
':hash' => $sha256Hash !== null && $sha256Hash !== '' ? $sha256Hash : null,
|
||||
':git' => $gitCommit !== null && $gitCommit !== '' ? $gitCommit : null,
|
||||
':size' => $sizeBytes,
|
||||
':manifest' => $manifestJson,
|
||||
':critical' => $isCritical ? 1 : 0,
|
||||
':slug' => $productSlug,
|
||||
':version' => $version,
|
||||
':channel' => $channel,
|
||||
':platform' => $platform,
|
||||
':notes' => $releaseNotes,
|
||||
':url' => $downloadUrl,
|
||||
':hash' => $sha256Hash !== null && $sha256Hash !== '' ? $sha256Hash : null,
|
||||
':git' => $gitCommit !== null && $gitCommit !== '' ? $gitCommit : null,
|
||||
':size' => $sizeBytes,
|
||||
':manifest' => $manifestJson,
|
||||
':signature' => $manifestSignature !== null && $manifestSignature !== '' ? $manifestSignature : null,
|
||||
':critical' => $isCritical ? 1 : 0,
|
||||
]);
|
||||
|
||||
$release = $this->findRelease($productSlug, $version, $channel);
|
||||
$release = $this->findRelease($productSlug, $version, $channel, $platform);
|
||||
$releaseId = $release !== null ? (int)$release['id'] : 0;
|
||||
|
||||
// Bugtracker-Items, die fuer genau diesen Build vorgemerkt sind,
|
||||
@@ -130,6 +222,7 @@ final class UpdateManager
|
||||
'project_slug' => $productSlug,
|
||||
'version' => $version,
|
||||
'channel' => $channel,
|
||||
'platform' => $platform,
|
||||
'is_critical' => $isCritical,
|
||||
'download_url' => $downloadUrl,
|
||||
'auto_resolved' => $autoResolved,
|
||||
@@ -139,10 +232,11 @@ final class UpdateManager
|
||||
}
|
||||
|
||||
Logger::info('Release veroeffentlicht', [
|
||||
'product' => $productSlug,
|
||||
'version' => $version,
|
||||
'channel' => $channel,
|
||||
'author' => $author,
|
||||
'product' => $productSlug,
|
||||
'version' => $version,
|
||||
'channel' => $channel,
|
||||
'platform' => $platform,
|
||||
'author' => $author,
|
||||
]);
|
||||
|
||||
return [
|
||||
@@ -152,14 +246,24 @@ final class UpdateManager
|
||||
];
|
||||
}
|
||||
|
||||
public function findRelease(string $productSlug, string $version, string $channel): ?array
|
||||
{
|
||||
public function findRelease(
|
||||
string $productSlug,
|
||||
string $version,
|
||||
string $channel,
|
||||
?string $platform = null
|
||||
): ?array {
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT * FROM updateservice_releases
|
||||
WHERE product_slug = :slug AND version = :version AND channel = :channel
|
||||
AND platform = :platform
|
||||
LIMIT 1
|
||||
');
|
||||
$stmt->execute([':slug' => $productSlug, ':version' => $version, ':channel' => $channel]);
|
||||
$stmt->execute([
|
||||
':slug' => $productSlug,
|
||||
':version' => $version,
|
||||
':channel' => $channel,
|
||||
':platform' => self::normalizePlatform($platform),
|
||||
]);
|
||||
$row = $stmt->fetch();
|
||||
return is_array($row) ? $row : null;
|
||||
}
|
||||
@@ -176,8 +280,12 @@ final class UpdateManager
|
||||
*
|
||||
* @return list<array<string,mixed>>
|
||||
*/
|
||||
public function getReleases(?string $productSlug = null, ?string $channel = null, int $limit = 200): array
|
||||
{
|
||||
public function getReleases(
|
||||
?string $productSlug = null,
|
||||
?string $channel = null,
|
||||
int $limit = 200,
|
||||
?string $platform = null
|
||||
): array {
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
@@ -190,17 +298,26 @@ final class UpdateManager
|
||||
$params[':channel'] = $channel;
|
||||
}
|
||||
|
||||
// Hier wird bewusst exakt gefiltert statt aufgeloest: dieser Endpunkt
|
||||
// listet den Bestand, er waehlt kein Paket aus.
|
||||
if ($platform !== null && $platform !== '') {
|
||||
$where[] = 'platform = :platform';
|
||||
$params[':platform'] = self::normalizePlatform($platform);
|
||||
}
|
||||
|
||||
$sql = 'SELECT * FROM updateservice_releases';
|
||||
if ($where !== []) {
|
||||
$sql .= ' WHERE ' . implode(' AND ', $where);
|
||||
}
|
||||
$sql .= ' ORDER BY product_slug ASC, channel ASC, created_at DESC LIMIT ' . max(1, min($limit, 1000));
|
||||
$sql .= ' ORDER BY product_slug ASC, channel ASC, platform ASC, created_at DESC LIMIT '
|
||||
. max(1, min($limit, 1000));
|
||||
|
||||
$stmt = $this->db->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$releases = $stmt->fetchAll() ?: [];
|
||||
|
||||
// Innerhalb einer Produkt/Kanal-Gruppe nach echter Versionsordnung sortieren.
|
||||
// Innerhalb einer Produkt/Kanal/Plattform-Gruppe nach echter
|
||||
// Versionsordnung sortieren.
|
||||
usort($releases, static function (array $a, array $b): int {
|
||||
$bySlug = strcmp((string)$a['product_slug'], (string)$b['product_slug']);
|
||||
if ($bySlug !== 0) {
|
||||
@@ -210,6 +327,13 @@ final class UpdateManager
|
||||
if ($byChannel !== 0) {
|
||||
return $byChannel;
|
||||
}
|
||||
$byPlatform = strcmp(
|
||||
(string)($a['platform'] ?? self::PLATFORM_ANY),
|
||||
(string)($b['platform'] ?? self::PLATFORM_ANY)
|
||||
);
|
||||
if ($byPlatform !== 0) {
|
||||
return $byPlatform;
|
||||
}
|
||||
return Version::compare((string)$b['version'], (string)$a['version']);
|
||||
});
|
||||
|
||||
|
||||
@@ -25,7 +25,11 @@ use PDO;
|
||||
* stuft Monitore entsprechend auf warning bzw. down. Zustandswechsel landen
|
||||
* im Event-Log und loesen Webhooks aus.
|
||||
*
|
||||
* Aufruf per Cron (empfohlen minuetlich):
|
||||
* Aufruf per Cron (empfohlen minuetlich), bevorzugt intern ohne HTTP:
|
||||
* * * * * * /usr/bin/php /pfad/zum/deploymentcenter/cli/tick.php --quiet
|
||||
*
|
||||
* Alternativ ueber die Schnittstelle, wenn der Cron auf einer anderen Maschine
|
||||
* laeuft als die Anwendung:
|
||||
* curl -H "Authorization: Bearer <SHARED_KEY>" https://dc.example.com/api/watchdog/v1/evaluate
|
||||
*/
|
||||
final class Evaluator
|
||||
|
||||
@@ -86,7 +86,8 @@ final class MonitorRepo
|
||||
?string $message,
|
||||
?string $groupKey = null,
|
||||
?string $os = null,
|
||||
$checks = null
|
||||
$checks = null,
|
||||
?string $appVersion = null
|
||||
): array {
|
||||
$metricsJson = (is_array($metrics) || is_object($metrics))
|
||||
? json_encode($metrics, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
|
||||
@@ -139,11 +140,11 @@ final class MonitorRepo
|
||||
INSERT INTO watchdog_monitors (
|
||||
source, instance, type, state, last_state_change_utc, expected_interval_sec,
|
||||
last_seen_utc, last_status, last_message, metrics_json, health_json,
|
||||
failing_checks, group_key, os, created_utc, updated_utc
|
||||
failing_checks, group_key, os, app_version, created_utc, updated_utc
|
||||
) VALUES (
|
||||
:source, :instance, :type, :state, UTC_TIMESTAMP(), :interval,
|
||||
UTC_TIMESTAMP(), :last_status, :message, :metrics, :health,
|
||||
:failing, :group_key, :os, UTC_TIMESTAMP(), UTC_TIMESTAMP()
|
||||
:failing, :group_key, :os, :app_version, UTC_TIMESTAMP(), UTC_TIMESTAMP()
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
-- Reihenfolge ist relevant: MySQL wertet die Zuweisungen von
|
||||
@@ -161,6 +162,9 @@ final class MonitorRepo
|
||||
failing_checks = VALUES(failing_checks),
|
||||
group_key = COALESCE(VALUES(group_key), group_key),
|
||||
os = COALESCE(VALUES(os), os),
|
||||
-- COALESCE, damit ein Agent, der die Version nicht mitschickt,
|
||||
-- eine frueher gemeldete nicht loescht.
|
||||
app_version = COALESCE(VALUES(app_version), app_version),
|
||||
updated_utc = VALUES(updated_utc)
|
||||
');
|
||||
|
||||
@@ -177,6 +181,7 @@ final class MonitorRepo
|
||||
':failing' => $failing !== [] ? mb_substr(implode(', ', $failing), 0, 255) : null,
|
||||
':group_key' => $groupKey,
|
||||
':os' => $os,
|
||||
':app_version' => $appVersion !== null ? mb_substr($appVersion, 0, 64) : null,
|
||||
]);
|
||||
|
||||
$monitor = $this->getMonitor($source, $instance);
|
||||
|
||||
Reference in New Issue
Block a user