From 82916167cfa5fd990a1c0aee76795988983656b0 Mon Sep 17 00:00:00 2001 From: nexxo Date: Sat, 25 Jul 2026 20:08:07 +0200 Subject: [PATCH] fix(monitoring): MONITORING_ATTEMPTS is now exact (off-by-one); accept int or dict monitor id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - attempt is 0-based, so compare one-based: ATTEMPTS=2 really means two tries - bridge accepts add_monitor returning {monitorID}|{id}|int (v1.2.1 returns a dict — re-verified: first create on a fresh Kuma is HTTP 200 with the id) Co-Authored-By: Claude Opus 4.8 --- .../Steps/Customer/RegisterMonitoring.php | 6 ++++-- docker/kuma-bridge/app.py | 12 ++++++++++-- docs/monitoring-uptime-kuma.md | 2 +- tests/Feature/Provisioning/CustomerStepsTest.php | 9 +++++---- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/app/Provisioning/Steps/Customer/RegisterMonitoring.php b/app/Provisioning/Steps/Customer/RegisterMonitoring.php index b4b0a1a..7ee6959 100644 --- a/app/Provisioning/Steps/Customer/RegisterMonitoring.php +++ b/app/Provisioning/Steps/Customer/RegisterMonitoring.php @@ -41,8 +41,10 @@ class RegisterMonitoring extends CustomerStep throw $e; // operator wants monitoring to gate delivery } - $allowed = (int) config('provisioning.monitoring.attempts', 2); - if ($run->attempt < $allowed) { + // `attempt` is 0-based (this call IS attempt number attempt+1), so + // compare one-based: MONITORING_ATTEMPTS=2 really means two tries. + $allowed = max(1, (int) config('provisioning.monitoring.attempts', 2)); + if ($run->attempt + 1 < $allowed) { return StepResult::retry(30, 'monitoring unavailable: '.$e->getMessage()); } diff --git a/docker/kuma-bridge/app.py b/docker/kuma-bridge/app.py index 72a699b..fb498b7 100644 --- a/docker/kuma-bridge/app.py +++ b/docker/kuma-bridge/app.py @@ -163,11 +163,19 @@ def create_monitor(payload: MonitorIn) -> dict: maxretries=payload.retries or 1, # Kuma's own field name ) ) - monitor_id = created.get("monitorID") if isinstance(created, dict) else None + # uptime-kuma-api 1.2.x returns {"msg": ..., "monitorID": N} (verified against + # Kuma 1.x). Accept a bare id too, so a future library change can't break us. + if isinstance(created, dict): + monitor_id = created.get("monitorID", created.get("id")) + elif isinstance(created, int): + monitor_id = created + else: + monitor_id = None + if monitor_id is None: raise HTTPException(status_code=502, detail=f"Kuma returned no monitor id: {created}") - return {"monitor": {"id": monitor_id}} + return {"monitor": {"id": int(monitor_id)}} @app.get("/monitors/{monitor_id}", dependencies=[Depends(require_token)]) diff --git a/docs/monitoring-uptime-kuma.md b/docs/monitoring-uptime-kuma.md index df67748..f6d9c08 100644 --- a/docs/monitoring-uptime-kuma.md +++ b/docs/monitoring-uptime-kuma.md @@ -64,7 +64,7 @@ Alle Monitor-Routen erfordern `Authorization: Bearer $BRIDGE_TOKEN`. Monitoring ist Beobachtung, nicht das Produkt. Fällt Kuma oder die Bridge aus, darf die Bereitstellung eines **bezahlten** Kunden nicht scheitern: -- Der Schritt `register_monitoring` versucht es `MONITORING_ATTEMPTS` mal (Standard 2), +- Der Schritt `register_monitoring` versucht es genau `MONITORING_ATTEMPTS` mal (Standard 2 = 1 Versuch + 1 Wiederholung), - danach läuft die Bereitstellung **degradiert** weiter und schreibt ein sichtbares `info`-Ereignis in die Admin-Konsole, - mit `MONITORING_REQUIRED=true` wird Monitoring stattdessen erzwungen (der Lauf diff --git a/tests/Feature/Provisioning/CustomerStepsTest.php b/tests/Feature/Provisioning/CustomerStepsTest.php index 563e403..d8323db 100644 --- a/tests/Feature/Provisioning/CustomerStepsTest.php +++ b/tests/Feature/Provisioning/CustomerStepsTest.php @@ -371,13 +371,14 @@ it('keeps provisioning when monitoring is unreachable (retry, then degrade)', fu $s['monitoring']->failWith = 'connection refused'; ['run' => $run, 'instance' => $instance] = reservedRun(); - // Early attempts retry — the outage may be transient. + // MONITORING_ATTEMPTS=2 means exactly two tries: attempt 0 retries… + config()->set('provisioning.monitoring.attempts', 2); $first = app(RegisterMonitoring::class)->execute($run->fresh()); expect($first->type)->toBe('retry')->and($first->reason)->toContain('connection refused'); - // Once the allowance is used up, continue DEGRADED with a visible event - // instead of failing a paid customer's provisioning. - $run->update(['attempt' => (int) config('provisioning.monitoring.attempts', 2)]); + // …and the SECOND try (attempt 1) is the last: continue DEGRADED with a + // visible event instead of failing a paid customer's provisioning. + $run->update(['attempt' => 1]); expect(app(RegisterMonitoring::class)->execute($run->fresh())->type)->toBe('advance'); expect($instance->fresh()->monitoringTargets()->count())->toBe(0)