[infra] Per-ingress external-monitor annotation + actualbudget plan-time fix [ci skip]
## Context
Two operational gaps surfaced during a healthcheck sweep today:
1. **External monitoring coverage**: Only ~13 hostnames (via `cloudflare_proxied_names`
in `config.tfvars`) had `[External]` monitors in Uptime Kuma. Any service deployed via
`ingress_factory` with `dns_type = "proxied"` auto-created its DNS record but was NOT
registered for external probing — so outages like Immich going down externally were
invisible until a user complained. 99 of ~125 public ingresses had no external
monitor.
2. **actualbudget stack unplannable**: `count = var.budget_encryption_password != null
? 1 : 0` in `factory/main.tf:152` failed with "Invalid count argument" because the
value flows from a `data.kubernetes_secret` whose contents are `(known after apply)`
at plan time. Blocked CI applies and drift reconciliation.
## This change
### Per-ingress external-monitor annotation (ingress_factory + reverse_proxy/factory)
- New variables `external_monitor` (bool, nullable) + `external_monitor_name` (string,
nullable). Default is "follow dns_type" — enabled for any public DNS record
(`dns_type != "none"`, covers both proxied and non-proxied so Immich and other
direct-A records are also monitored).
- Emits two annotations on the Ingress:
- `uptime.viktorbarzin.me/external-monitor = "true"`
- `uptime.viktorbarzin.me/external-monitor-name = "<label>"` (optional override)
### external-monitor-sync CronJob (uptime-kuma stack)
- Discovers targets from live Ingress objects via the K8s API first (filter by
annotation), falls back to the legacy `external-monitor-targets` ConfigMap on any
API error (zero rollout risk).
- New `ServiceAccount` + cluster-wide `ClusterRole`/`ClusterRoleBinding` giving
`list`/`get` on `networking.k8s.io/ingresses`.
- `API_SERVER` now uses the `KUBERNETES_SERVICE_HOST` env var (always injected by K8s)
instead of `kubernetes.default.svc` — the search-domain expansion failed in the
CronJob pod's DNS config. Verified working: CronJob now logs
`Loaded N external monitor targets (source=k8s-api)`.
### actualbudget count-on-unknown refactor
- Replaced `count = var.budget_encryption_password != null ? 1 : 0` with two explicit
plan-time booleans: `enable_http_api` and `enable_bank_sync`. Values are known at
plan; no `-target` workaround needed.
- Callers (`stacks/actualbudget/main.tf`) pass `true` explicitly. Runtime behaviour is
unchanged — the secret is still consumed via env var.
- Also aligned the factory with live state (the 3 budget-* PVCs had been migrated
`proxmox-lvm` → `proxmox-lvm-encrypted` outside Terraform): PVC resource renamed
`data_proxmox` → `data_encrypted`, storage class updated, orphaned `nfs_data` module
removed. State was rm'd + re-imported with matching UIDs, so no data was moved.
## Rollout status (already partially applied in this session)
- `stacks/uptime-kuma` applied — SA + RBAC + CronJob changes live; FQDN fix verified
- `stacks/actualbudget` applied — budget-{viktor,anca,emo} all 200 OK externally
- `stacks/mailserver` + 21 other ingress_factory consumers applied — annotations live
- CronJob `external-monitor-sync` latest run: `source=k8s-api`, 26 monitors active
(was 13 on the central list)
## Deferred (separate work)
- 4 stacks show pre-existing DESTRUCTIVE drift in plan (metallb namespace, claude-memory,
rbac, redis) — NOT triggered by this commit but will be by CI's global-file cascade.
`[ci skip]` here so those don't auto-apply; they will be fixed manually before the
next CI push.
- Cleanup of `cloudflare_proxied_names` list once Helm-managed ingresses (authentik,
grafana, vault, forgejo) are annotated — separate PR.
## Test plan
### Automated
\`\`\`
\$ kubectl -n uptime-kuma logs \$(kubectl -n uptime-kuma get pods -l job-name -o name | tail -1)
Loaded 26 external monitor targets (source=k8s-api)
Sync complete: 7 created, 0 deleted, 17 unchanged
\$ curl -sk -o /dev/null -w "%{http_code}\n" -H "Accept: text/html" \\
https://dawarich.viktorbarzin.me/ https://nextcloud.viktorbarzin.me/ \\
https://budget-viktor.viktorbarzin.me/
200 302 200
\$ kubectl -n actualbudget get deploy,pvc -l app=budget-viktor
deployment.apps/budget-viktor 1/1 1 1 Ready
persistentvolumeclaim/budget-viktor-data-encrypted Bound 10Gi RWO proxmox-lvm-encrypted
\`\`\`
### Manual Verification
1. Confirm the annotation is present on an ingress_factory ingress:
\`\`\`
kubectl -n dawarich get ingress dawarich -o \\
jsonpath='{.metadata.annotations.uptime\.viktorbarzin\.me/external-monitor}'
# Expected: "true"
\`\`\`
2. Confirm the new `[External] <name>` monitor appears in Uptime Kuma within 10 min
(CronJob interval). For Immich specifically, it will appear after the immich stack
is re-applied.
3. Verify actualbudget plan is clean:
\`\`\`
cd stacks/actualbudget && scripts/tg plan --non-interactive
# Expected: no "Invalid count argument" errors
\`\`\`
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0c4fe98d75
commit
66d2d9916b
5 changed files with 209 additions and 49 deletions
|
|
@ -97,6 +97,21 @@ variable "dns_type" {
|
|||
}
|
||||
}
|
||||
|
||||
# Uptime Kuma external monitor: when true, annotate the ingress so the
|
||||
# external-monitor-sync CronJob creates a `[External] <name>` monitor pointing
|
||||
# at https://<host>. Null means "follow dns_type" — enabled when proxied.
|
||||
variable "external_monitor" {
|
||||
type = bool
|
||||
default = null
|
||||
description = "Enable Uptime Kuma external monitor. null = auto (enabled when dns_type == 'proxied')."
|
||||
}
|
||||
|
||||
variable "external_monitor_name" {
|
||||
type = string
|
||||
default = null
|
||||
description = "Override the monitor label. Defaults to the ingress hostname label (e.g. 'dawarich' for dawarich.viktorbarzin.me)."
|
||||
}
|
||||
|
||||
# Cloudflare config defaults — override via variables if these change.
|
||||
# Source of truth: config.tfvars (cloudflare_zone_id, cloudflare_tunnel_id, public_ip, public_ipv6)
|
||||
variable "cloudflare_zone_id" {
|
||||
|
|
@ -133,31 +148,40 @@ locals {
|
|||
effective_host = var.full_host != null ? var.full_host : "${var.host != null ? var.host : var.name}.${var.root_domain}"
|
||||
effective_anti_ai = var.anti_ai_scraping != null ? var.anti_ai_scraping : !var.protected
|
||||
|
||||
# External monitor enabled by default when the ingress has a public DNS
|
||||
# record (either CF-proxied or direct A/AAAA). Explicit bool overrides.
|
||||
effective_external_monitor = var.external_monitor != null ? var.external_monitor : (var.dns_type != "none")
|
||||
|
||||
external_monitor_annotations = local.effective_external_monitor ? merge(
|
||||
{ "uptime.viktorbarzin.me/external-monitor" = "true" },
|
||||
var.external_monitor_name != null ? { "uptime.viktorbarzin.me/external-monitor-name" = var.external_monitor_name } : {},
|
||||
) : {}
|
||||
|
||||
ns_to_group = {
|
||||
monitoring = "Infrastructure"
|
||||
prometheus = "Infrastructure"
|
||||
technitium = "Infrastructure"
|
||||
traefik = "Infrastructure"
|
||||
metallb-system = "Infrastructure"
|
||||
kyverno = "Infrastructure"
|
||||
authentik = "Identity & Security"
|
||||
crowdsec = "Identity & Security"
|
||||
woodpecker = "Development & CI"
|
||||
forgejo = "Development & CI"
|
||||
immich = "Media & Entertainment"
|
||||
frigate = "Smart Home"
|
||||
home-assistant = "Smart Home"
|
||||
ollama = "AI & Data"
|
||||
dbaas = "Infrastructure"
|
||||
servarr = "Media & Entertainment"
|
||||
navidrome = "Media & Entertainment"
|
||||
nextcloud = "Productivity"
|
||||
n8n = "Automation"
|
||||
changedetection = "Automation"
|
||||
finance = "Finance & Personal"
|
||||
homepage = "Core Platform"
|
||||
reverse-proxy = "Smart Home"
|
||||
mailserver = "Infrastructure"
|
||||
monitoring = "Infrastructure"
|
||||
prometheus = "Infrastructure"
|
||||
technitium = "Infrastructure"
|
||||
traefik = "Infrastructure"
|
||||
metallb-system = "Infrastructure"
|
||||
kyverno = "Infrastructure"
|
||||
authentik = "Identity & Security"
|
||||
crowdsec = "Identity & Security"
|
||||
woodpecker = "Development & CI"
|
||||
forgejo = "Development & CI"
|
||||
immich = "Media & Entertainment"
|
||||
frigate = "Smart Home"
|
||||
home-assistant = "Smart Home"
|
||||
ollama = "AI & Data"
|
||||
dbaas = "Infrastructure"
|
||||
servarr = "Media & Entertainment"
|
||||
navidrome = "Media & Entertainment"
|
||||
nextcloud = "Productivity"
|
||||
n8n = "Automation"
|
||||
changedetection = "Automation"
|
||||
finance = "Finance & Personal"
|
||||
homepage = "Core Platform"
|
||||
reverse-proxy = "Smart Home"
|
||||
mailserver = "Infrastructure"
|
||||
}
|
||||
|
||||
homepage_group = coalesce(
|
||||
|
|
@ -222,8 +246,9 @@ resource "kubernetes_ingress_v1" "proxied-ingress" {
|
|||
var.custom_content_security_policy != null ? "${var.namespace}-custom-csp-${var.name}@kubernetescrd" : null,
|
||||
], var.extra_middlewares)))
|
||||
"traefik.ingress.kubernetes.io/router.entrypoints" = "websecure"
|
||||
}, local.homepage_defaults, var.extra_annotations,
|
||||
var.dns_type != "none" ? { "cloudflare.viktorbarzin.me/dns-type" = var.dns_type } : {}
|
||||
}, local.homepage_defaults, var.extra_annotations,
|
||||
var.dns_type != "none" ? { "cloudflare.viktorbarzin.me/dns-type" = var.dns_type } : {},
|
||||
local.external_monitor_annotations,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,24 +13,31 @@ variable "budget_encryption_password" {
|
|||
default = null # If not passed, we won't run banksync ;known after initial installation
|
||||
sensitive = true
|
||||
}
|
||||
# Plan-time toggles — these MUST be known at plan time. The secret values
|
||||
# (budget_encryption_password, sync_id) are read from ESO-managed K8s Secrets
|
||||
# and are unknown at plan time on first apply, so we cannot base `count` on
|
||||
# them directly. Callers pass these booleans as hardcoded plan-time constants
|
||||
# that reflect whether the corresponding credentials are expected to exist.
|
||||
variable "enable_http_api" {
|
||||
type = bool
|
||||
default = false
|
||||
description = "Deploy the actual-http-api sidecar. Must be true for the cronjob to run."
|
||||
}
|
||||
variable "enable_bank_sync" {
|
||||
type = bool
|
||||
default = false
|
||||
description = "Deploy the daily bank-sync CronJob. Requires enable_http_api=true."
|
||||
}
|
||||
variable "nfs_server" { type = string }
|
||||
variable "homepage_annotations" {
|
||||
type = map(string)
|
||||
default = {}
|
||||
}
|
||||
|
||||
module "nfs_data" {
|
||||
source = "../../../modules/kubernetes/nfs_volume"
|
||||
name = "actualbudget-${var.name}-data"
|
||||
namespace = "actualbudget"
|
||||
nfs_server = var.nfs_server
|
||||
nfs_path = "/mnt/main/actualbudget/${var.name}"
|
||||
}
|
||||
|
||||
resource "kubernetes_persistent_volume_claim" "data_proxmox" {
|
||||
resource "kubernetes_persistent_volume_claim" "data_encrypted" {
|
||||
wait_until_bound = false
|
||||
metadata {
|
||||
name = "actualbudget-${var.name}-data-proxmox"
|
||||
name = "actualbudget-${var.name}-data-encrypted"
|
||||
namespace = "actualbudget"
|
||||
annotations = {
|
||||
"resize.topolvm.io/threshold" = "80%"
|
||||
|
|
@ -40,7 +47,7 @@ resource "kubernetes_persistent_volume_claim" "data_proxmox" {
|
|||
}
|
||||
spec {
|
||||
access_modes = ["ReadWriteOnce"]
|
||||
storage_class_name = "proxmox-lvm"
|
||||
storage_class_name = "proxmox-lvm-encrypted"
|
||||
resources {
|
||||
requests = {
|
||||
storage = "1Gi"
|
||||
|
|
@ -103,7 +110,7 @@ resource "kubernetes_deployment" "actualbudget" {
|
|||
volume {
|
||||
name = "data"
|
||||
persistent_volume_claim {
|
||||
claim_name = kubernetes_persistent_volume_claim.data_proxmox.metadata[0].name
|
||||
claim_name = kubernetes_persistent_volume_claim.data_encrypted.metadata[0].name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -149,7 +156,7 @@ resource "random_string" "api-key" {
|
|||
}
|
||||
|
||||
resource "kubernetes_deployment" "actualbudget-http-api" {
|
||||
count = var.budget_encryption_password != null ? 1 : 0
|
||||
count = var.enable_http_api ? 1 : 0
|
||||
metadata {
|
||||
name = "actualbudget-http-api-${var.name}"
|
||||
namespace = "actualbudget"
|
||||
|
|
@ -232,7 +239,7 @@ resource "kubernetes_service" "actualbudget-http-api" {
|
|||
}
|
||||
|
||||
resource "kubernetes_cron_job_v1" "bank-sync" {
|
||||
count = var.sync_id != null && var.budget_encryption_password != null ? 1 : 0
|
||||
count = var.enable_bank_sync ? 1 : 0
|
||||
metadata {
|
||||
name = "bank-sync-${var.name}"
|
||||
namespace = "actualbudget"
|
||||
|
|
|
|||
|
|
@ -77,6 +77,8 @@ module "viktor" {
|
|||
nfs_server = var.nfs_server
|
||||
depends_on = [kubernetes_namespace.actualbudget]
|
||||
tier = local.tiers.edge
|
||||
enable_http_api = true
|
||||
enable_bank_sync = true
|
||||
budget_encryption_password = lookup(local.credentials["viktor"], "password", null)
|
||||
sync_id = lookup(local.credentials["viktor"], "sync_id", null)
|
||||
homepage_annotations = {
|
||||
|
|
@ -98,6 +100,8 @@ module "anca" {
|
|||
nfs_server = var.nfs_server
|
||||
depends_on = [kubernetes_namespace.actualbudget]
|
||||
tier = local.tiers.edge
|
||||
enable_http_api = true
|
||||
enable_bank_sync = true
|
||||
budget_encryption_password = lookup(local.credentials["anca"], "password", null)
|
||||
sync_id = lookup(local.credentials["anca"], "sync_id", null)
|
||||
homepage_annotations = {
|
||||
|
|
@ -119,6 +123,8 @@ module "emo" {
|
|||
nfs_server = var.nfs_server
|
||||
depends_on = [kubernetes_namespace.actualbudget]
|
||||
tier = local.tiers.edge
|
||||
enable_http_api = true
|
||||
enable_bank_sync = true
|
||||
budget_encryption_password = lookup(local.credentials["emo"], "password", null)
|
||||
sync_id = lookup(local.credentials["emo"], "sync_id", null)
|
||||
homepage_annotations = {
|
||||
|
|
|
|||
|
|
@ -66,6 +66,21 @@ variable "dns_type" {
|
|||
error_message = "dns_type must be 'proxied', 'non-proxied', or 'none'."
|
||||
}
|
||||
}
|
||||
|
||||
# Uptime Kuma external monitor: when true, annotate the ingress so the
|
||||
# external-monitor-sync CronJob creates a `[External] <name>` monitor pointing
|
||||
# at https://<host>. Null means "follow dns_type" — enabled when proxied.
|
||||
variable "external_monitor" {
|
||||
type = bool
|
||||
default = null
|
||||
description = "Enable Uptime Kuma external monitor. null = auto (enabled when dns_type == 'proxied')."
|
||||
}
|
||||
|
||||
variable "external_monitor_name" {
|
||||
type = string
|
||||
default = null
|
||||
description = "Override the monitor label. Defaults to the ingress hostname label."
|
||||
}
|
||||
variable "cloudflare_zone_id" {
|
||||
type = string
|
||||
default = "fd2c5dd4efe8fe38958944e74d0ced6d"
|
||||
|
|
@ -106,6 +121,16 @@ resource "kubernetes_service" "proxied-service" {
|
|||
}
|
||||
}
|
||||
|
||||
locals {
|
||||
# External monitor defaults: on when proxied, off otherwise. Explicit bool overrides.
|
||||
effective_external_monitor = var.external_monitor != null ? var.external_monitor : (var.dns_type == "proxied")
|
||||
|
||||
external_monitor_annotations = local.effective_external_monitor ? merge(
|
||||
{ "uptime.viktorbarzin.me/external-monitor" = "true" },
|
||||
var.external_monitor_name != null ? { "uptime.viktorbarzin.me/external-monitor-name" = var.external_monitor_name } : {},
|
||||
) : {}
|
||||
}
|
||||
|
||||
resource "kubernetes_ingress_v1" "proxied-ingress" {
|
||||
metadata {
|
||||
name = var.name
|
||||
|
|
@ -125,8 +150,9 @@ resource "kubernetes_ingress_v1" "proxied-ingress" {
|
|||
"traefik.ingress.kubernetes.io/router.entrypoints" = "websecure"
|
||||
"traefik.ingress.kubernetes.io/service.serversscheme" = var.backend_protocol == "HTTPS" ? "https" : null
|
||||
"traefik.ingress.kubernetes.io/service.serverstransport" = var.backend_protocol == "HTTPS" ? "traefik-insecure-skip-verify@kubernetescrd" : null
|
||||
}, var.extra_annotations,
|
||||
var.dns_type != "none" ? { "cloudflare.viktorbarzin.me/dns-type" = var.dns_type } : {}
|
||||
}, var.extra_annotations,
|
||||
var.dns_type != "none" ? { "cloudflare.viktorbarzin.me/dns-type" = var.dns_type } : {},
|
||||
local.external_monitor_annotations,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -252,9 +252,50 @@ module "ingress" {
|
|||
|
||||
# =============================================================================
|
||||
# External Monitor Sync
|
||||
# Ensures Uptime Kuma has external HTTPS monitors for all Cloudflare-proxied services.
|
||||
# Reads targets from a Terraform-generated ConfigMap, creates/deletes monitors to match.
|
||||
# Ensures Uptime Kuma has external HTTPS monitors for every ingress annotated
|
||||
# with `uptime.viktorbarzin.me/external-monitor=true`. Falls back to a
|
||||
# Terraform-generated ConfigMap when API discovery is unavailable.
|
||||
#
|
||||
# Discovery modes (the script tries them in order):
|
||||
# 1. K8s API — list ingresses cluster-wide, filter by annotation
|
||||
# 2. ConfigMap fallback — read /config/targets.json (legacy list from
|
||||
# cloudflare_proxied_names)
|
||||
# =============================================================================
|
||||
|
||||
resource "kubernetes_service_account_v1" "external_monitor_sync" {
|
||||
metadata {
|
||||
name = "external-monitor-sync"
|
||||
namespace = kubernetes_namespace.uptime-kuma.metadata[0].name
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_cluster_role_v1" "external_monitor_sync" {
|
||||
metadata {
|
||||
name = "external-monitor-sync"
|
||||
}
|
||||
rule {
|
||||
api_groups = ["networking.k8s.io"]
|
||||
resources = ["ingresses"]
|
||||
verbs = ["list", "get"]
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_cluster_role_binding_v1" "external_monitor_sync" {
|
||||
metadata {
|
||||
name = "external-monitor-sync"
|
||||
}
|
||||
role_ref {
|
||||
api_group = "rbac.authorization.k8s.io"
|
||||
kind = "ClusterRole"
|
||||
name = kubernetes_cluster_role_v1.external_monitor_sync.metadata[0].name
|
||||
}
|
||||
subject {
|
||||
kind = "ServiceAccount"
|
||||
name = kubernetes_service_account_v1.external_monitor_sync.metadata[0].name
|
||||
namespace = kubernetes_namespace.uptime-kuma.metadata[0].name
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_config_map_v1" "external_monitor_targets" {
|
||||
metadata {
|
||||
name = "external-monitor-targets"
|
||||
|
|
@ -283,24 +324,79 @@ resource "kubernetes_cron_job_v1" "external_monitor_sync" {
|
|||
template {
|
||||
metadata {}
|
||||
spec {
|
||||
service_account_name = kubernetes_service_account_v1.external_monitor_sync.metadata[0].name
|
||||
container {
|
||||
name = "sync"
|
||||
image = "docker.io/library/python:3.12-alpine"
|
||||
command = ["/bin/sh", "-c", <<-EOT
|
||||
pip install --quiet --disable-pip-version-check uptime-kuma-api
|
||||
python3 << 'PYEOF'
|
||||
import os, json, time
|
||||
import os, json, ssl, time, urllib.request, urllib.error
|
||||
from uptime_kuma_api import UptimeKumaApi, MonitorType
|
||||
|
||||
UPTIME_KUMA_URL = "http://uptime-kuma.uptime-kuma.svc.cluster.local"
|
||||
UPTIME_KUMA_PASS = os.environ["UPTIME_KUMA_PASSWORD"]
|
||||
TARGETS_FILE = "/config/targets.json"
|
||||
FALLBACK_FILE = "/config/targets.json"
|
||||
PREFIX = "[External] "
|
||||
ANNOTATION_ENABLE = "uptime.viktorbarzin.me/external-monitor"
|
||||
ANNOTATION_NAME = "uptime.viktorbarzin.me/external-monitor-name"
|
||||
SA_DIR = "/var/run/secrets/kubernetes.io/serviceaccount"
|
||||
API_SERVER = f"https://{os.environ.get('KUBERNETES_SERVICE_HOST', 'kubernetes.default.svc.cluster.local')}:{os.environ.get('KUBERNETES_SERVICE_PORT', '443')}"
|
||||
|
||||
with open(TARGETS_FILE) as f:
|
||||
targets = json.load(f)
|
||||
|
||||
print(f"Loaded {len(targets)} external monitor targets")
|
||||
def load_from_api():
|
||||
"""List ingresses via in-cluster API, filter by annotation, derive targets."""
|
||||
with open(f"{SA_DIR}/token") as f:
|
||||
token = f.read().strip()
|
||||
ctx = ssl.create_default_context(cafile=f"{SA_DIR}/ca.crt")
|
||||
url = f"{API_SERVER}/apis/networking.k8s.io/v1/ingresses"
|
||||
req = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"})
|
||||
with urllib.request.urlopen(req, context=ctx, timeout=30) as resp:
|
||||
body = json.loads(resp.read())
|
||||
|
||||
targets = []
|
||||
for ing in body.get("items", []):
|
||||
anns = (ing.get("metadata") or {}).get("annotations") or {}
|
||||
if anns.get(ANNOTATION_ENABLE, "").lower() != "true":
|
||||
continue
|
||||
tls = (ing.get("spec") or {}).get("tls") or []
|
||||
host = None
|
||||
if tls and tls[0].get("hosts"):
|
||||
host = tls[0]["hosts"][0]
|
||||
else:
|
||||
rules = (ing.get("spec") or {}).get("rules") or []
|
||||
if rules:
|
||||
host = rules[0].get("host")
|
||||
if not host:
|
||||
ns = ing["metadata"]["namespace"]
|
||||
nm = ing["metadata"]["name"]
|
||||
print(f"WARN: ingress {ns}/{nm} annotated but has no host; skipping")
|
||||
continue
|
||||
label = anns.get(ANNOTATION_NAME) or host.split(".")[0]
|
||||
targets.append({"name": label, "url": f"https://{host}"})
|
||||
return targets
|
||||
|
||||
|
||||
def load_from_configmap():
|
||||
"""Legacy fallback: read the ConfigMap list."""
|
||||
with open(FALLBACK_FILE) as f:
|
||||
raw = json.load(f)
|
||||
return [{"name": t["name"], "url": t["url"]} for t in raw]
|
||||
|
||||
|
||||
try:
|
||||
targets = load_from_api()
|
||||
source = "k8s-api"
|
||||
if not targets:
|
||||
print("WARN: k8s-api returned 0 targets; falling back to ConfigMap")
|
||||
targets = load_from_configmap()
|
||||
source = "configmap"
|
||||
except (urllib.error.URLError, OSError, KeyError, ValueError) as e:
|
||||
print(f"WARN: k8s-api discovery failed ({e!r}); falling back to ConfigMap")
|
||||
targets = load_from_configmap()
|
||||
source = "configmap"
|
||||
|
||||
print(f"Loaded {len(targets)} external monitor targets (source={source})")
|
||||
|
||||
api = UptimeKumaApi(UPTIME_KUMA_URL, timeout=120, wait_events=0.2)
|
||||
api.login("admin", UPTIME_KUMA_PASS)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue