diff --git a/argos/certs/certificate-argos-panel.yaml b/argos/certs/certificate-argos-panel.yaml deleted file mode 100644 index 002bfee..0000000 --- a/argos/certs/certificate-argos-panel.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: cert-manager.io/v1 -kind: Certificate -metadata: - name: panel-argos-cert - namespace: argos-core -spec: - secretName: panel-argos-tls - issuerRef: - name: letsencrypt-prod - kind: ClusterIssuer - dnsNames: - - panel.argos.c2et.net diff --git a/argos/configmaps/configmap-mediamtx.yaml b/argos/configmaps/configmap-mediamtx.yaml index 3285b67..778ba0f 100644 --- a/argos/configmaps/configmap-mediamtx.yaml +++ b/argos/configmaps/configmap-mediamtx.yaml @@ -11,5 +11,4 @@ data: hls: no webrtc: yes paths: - all: - sourceOnDemand: yes + all: {} diff --git a/argos/configmaps/configmap-mosquitto.yaml b/argos/configmaps/configmap-mosquitto.yaml index 9e55849..5f7ced3 100644 --- a/argos/configmaps/configmap-mosquitto.yaml +++ b/argos/configmaps/configmap-mosquitto.yaml @@ -1,9 +1,11 @@ apiVersion: v1 kind: ConfigMap metadata: - name: mosquitto-conf + name: mosquitto-config namespace: argos-core data: mosquitto.conf: | + persistence true + persistence_location /mosquitto/data/ listener 1883 0.0.0.0 allow_anonymous true diff --git a/argos/configmaps/configmap-orchestrator.yaml b/argos/configmaps/configmap-orchestrator.yaml index 76f0d7f..f9cd57d 100644 --- a/argos/configmaps/configmap-orchestrator.yaml +++ b/argos/configmaps/configmap-orchestrator.yaml @@ -6,11 +6,14 @@ metadata: data: settings.yaml: | mqtt: - host: mqtt.argos.interna - port: 1883 + host: mosquitto.argos-core.svc.cluster.local topic: frigate/events + topic_base: argos/alerts + port: 1883 client_id: argos-core minio: + endpoint: minio.argos-core.svc.cluster.local:9000 + secure: false bucket: argos region: us-east-1 edges: diff --git a/argos/configmaps/configmap-panel.yaml b/argos/configmaps/configmap-panel.yaml index 4404c50..8f849aa 100644 --- a/argos/configmaps/configmap-panel.yaml +++ b/argos/configmaps/configmap-panel.yaml @@ -5,83 +5,136 @@ metadata: namespace: argos-core data: app.py: | - import os, sqlite3, time - from fastapi import FastAPI, HTTPException - from fastapi.responses import HTMLResponse + import os + from fastapi import FastAPI, HTTPException, Query + from fastapi.responses import HTMLResponse, StreamingResponse, JSONResponse from minio import Minio - from urllib.parse import urlparse + from urllib.parse import quote + from datetime import timezone + import io - DB="/data/argos.db" - mc=Minio(os.getenv("MINIO_ENDPOINT","s3.argos.interna"), - access_key=os.getenv("MINIO_ACCESS_KEY"), - secret_key=os.getenv("MINIO_SECRET_KEY"), - secure=os.getenv("MINIO_SECURE","false").lower()=="true") - app=FastAPI() + BUCKET = os.getenv("MINIO_BUCKET", "argos") + endpoint = os.getenv("MINIO_ENDPOINT", "minio.argos-core.svc.cluster.local:9000") + access_key = os.getenv("MINIO_ACCESS_KEY") + secret_key = os.getenv("MINIO_SECRET_KEY") + secure = os.getenv("MINIO_SECURE", "false").lower() == "true" - def rows(limit=100, camera=None, since=None): - q="SELECT id, ts, edge, camera, label, s3url, thumb_s3 FROM events" - cond=[]; args=[] - if camera: cond.append("camera=?"); args.append(camera) - if since: cond.append("ts>=?"); args.append(int(since)) - if cond: q+=" WHERE "+ " AND ".join(cond) - q+=" ORDER BY ts DESC LIMIT ?"; args.append(limit) - con=sqlite3.connect(DB); cur=con.cursor() - cur.execute(q, tuple(args)); r=cur.fetchall(); con.close() - return r + mc = Minio(endpoint, access_key=access_key, secret_key=secret_key, secure=secure) + + app = FastAPI() + + @app.get("/health") + def health(): + # simple check contra MinIO + try: + mc.list_buckets() + return {"ok": True} + except Exception as e: + return {"ok": False, "error": str(e)} + + def _iter_events(prefix: str | None, limit: int): + # list_objects devuelve en orden “no garantizado”; recolectamos y ordenamos por last_modified desc + objs = [] + for obj in mc.list_objects(BUCKET, prefix=prefix, recursive=True): + # omitimos directorios “lógicos” y thumbs por defecto en el listado principal + if obj.is_dir: + continue + if "/thumbs/" in obj.object_name: + continue + objs.append(obj) + objs.sort(key=lambda o: o.last_modified or 0, reverse=True) + return objs[:limit] @app.get("/api/events") - def api_events(limit:int=100, camera:str=None, since:int=None): - return [dict(id=i, ts=t, edge=e, camera=c, label=l or "", s3url=s, thumb=th or "") - for (i,t,e,c,l,s,th) in rows(limit,camera,since)] + def api_events(prefix: str | None = Query(default=None), limit: int = 50): + data = [] + for o in _iter_events(prefix, limit): + ts = None + if o.last_modified: + ts = o.last_modified.astimezone(timezone.utc).isoformat() + # si existe thumb, la inferimos (cam/... → cam/thumbs/...) + thumb = None + parts = o.object_name.split("/") + if len(parts) >= 2: + maybe = f"{parts[0]}/thumbs/" + parts[-1].rsplit(".",1)[0] + ".jpg" + try: + mc.stat_object(BUCKET, maybe) + thumb = maybe + except: + pass + data.append({ + "key": o.object_name, + "size": o.size, + "last_modified": ts, + "thumb": thumb + }) + return JSONResponse(data) - @app.get("/api/url/{event_id}") - def presign(event_id: str, expires: int = 600): - con=sqlite3.connect(DB); cur=con.cursor() - cur.execute("SELECT s3url FROM events WHERE id=?", (event_id,)) - row=cur.fetchone(); con.close() - if not row: raise HTTPException(404, "Not found") - s3url=row[0]; p=urlparse(s3url); b=p.netloc; k=p.path.lstrip("/") - return {"url": mc.presigned_get_object(b, k, expires=expires)} + @app.get("/file") + def file(key: str): + try: + resp = mc.get_object(BUCKET, key) + except Exception as e: + raise HTTPException(status_code=404, detail=f"not found: {e}") + # stream al cliente sin exponer MinIO + def gen(): + for d in resp.stream(32*1024): + yield d + # tipo de contenido básico por extensión + ctype = "application/octet-stream" + k = key.lower() + if k.endswith(".mp4"): ctype = "video/mp4" + elif k.endswith(".jpg") or k.endswith(".jpeg"): ctype = "image/jpeg" + return StreamingResponse(gen(), media_type=ctype) + + @app.get("/view", response_class=HTMLResponse) + def view(key: str): + keyq = quote(key) + return f""" + + Ver {key} + + +

{key}

+ +

Descargar

+

Volver

+ + """ @app.get("/", response_class=HTMLResponse) - def index(): - return """ - ARGOS Panel - -

ARGOS – Alarmas

-
-
- + def home(prefix: str | None = None, limit: int = 20): + # HTML mínimo que consume /api/events y pinta thumbs/links + p = f"&prefix={quote(prefix)}" if prefix else "" + items = [] + for e in _iter_events(prefix, limit): + thumb_html = "" + # intentar thumb + parts = e.object_name.split("/") + if len(parts) >= 2: + maybe = f"{parts[0]}/thumbs/" + parts[-1].rsplit(".",1)[0] + ".jpg" + try: + mc.stat_object(BUCKET, maybe) + thumb_html = f'' + except: + pass + items.append(f""" +
  • + {thumb_html} + {e.object_name} +
  • + """) + items_html = "\n".join(items) or "

    Sin eventos aún.

    " + return f""" + Argos Panel + + +

    Argos Panel

    +
    + + + +
    + + """ diff --git a/argos/configmaps/configmap-panel.yaml.old b/argos/configmaps/configmap-panel.yaml.old new file mode 100644 index 0000000..4404c50 --- /dev/null +++ b/argos/configmaps/configmap-panel.yaml.old @@ -0,0 +1,87 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: argos-panel-config + namespace: argos-core +data: + app.py: | + import os, sqlite3, time + from fastapi import FastAPI, HTTPException + from fastapi.responses import HTMLResponse + from minio import Minio + from urllib.parse import urlparse + + DB="/data/argos.db" + mc=Minio(os.getenv("MINIO_ENDPOINT","s3.argos.interna"), + access_key=os.getenv("MINIO_ACCESS_KEY"), + secret_key=os.getenv("MINIO_SECRET_KEY"), + secure=os.getenv("MINIO_SECURE","false").lower()=="true") + app=FastAPI() + + def rows(limit=100, camera=None, since=None): + q="SELECT id, ts, edge, camera, label, s3url, thumb_s3 FROM events" + cond=[]; args=[] + if camera: cond.append("camera=?"); args.append(camera) + if since: cond.append("ts>=?"); args.append(int(since)) + if cond: q+=" WHERE "+ " AND ".join(cond) + q+=" ORDER BY ts DESC LIMIT ?"; args.append(limit) + con=sqlite3.connect(DB); cur=con.cursor() + cur.execute(q, tuple(args)); r=cur.fetchall(); con.close() + return r + + @app.get("/api/events") + def api_events(limit:int=100, camera:str=None, since:int=None): + return [dict(id=i, ts=t, edge=e, camera=c, label=l or "", s3url=s, thumb=th or "") + for (i,t,e,c,l,s,th) in rows(limit,camera,since)] + + @app.get("/api/url/{event_id}") + def presign(event_id: str, expires: int = 600): + con=sqlite3.connect(DB); cur=con.cursor() + cur.execute("SELECT s3url FROM events WHERE id=?", (event_id,)) + row=cur.fetchone(); con.close() + if not row: raise HTTPException(404, "Not found") + s3url=row[0]; p=urlparse(s3url); b=p.netloc; k=p.path.lstrip("/") + return {"url": mc.presigned_get_object(b, k, expires=expires)} + + @app.get("/", response_class=HTMLResponse) + def index(): + return """ + ARGOS Panel + +

    ARGOS – Alarmas

    +
    +
    + + """ diff --git a/argos/deployments/deploy-mediamtx.yaml b/argos/deployments/deploy-mediamtx.yaml index a92fd19..9cdb62b 100644 --- a/argos/deployments/deploy-mediamtx.yaml +++ b/argos/deployments/deploy-mediamtx.yaml @@ -5,29 +5,33 @@ metadata: namespace: argos-core spec: replicas: 1 - selector: { matchLabels: { app: mediamtx } } + selector: + matchLabels: { app: mediamtx } template: - metadata: { labels: { app: mediamtx } } + metadata: + labels: { app: mediamtx } spec: - hostNetwork: true - dnsPolicy: ClusterFirstWithHostNet containers: - name: mediamtx image: bluenviron/mediamtx:1.14.0 - command: ["/bin/sh","-c"] - args: - - | - set -e - ulimit -n 1048576 - exec mediamtx /config/mediamtx.yml + # Nada de /bin/sh: solo pasa la ruta del YAML como argumento posicional + args: ["/config/mediamtx.yml"] volumeMounts: - name: cfg mountPath: /config ports: - - containerPort: 8554 # RTSP - - containerPort: 8189 # SRT - - containerPort: 8889 # WHIP - - containerPort: 8880 # HTTP/API + - name: rtsp + containerPort: 8554 + protocol: TCP + - name: http + containerPort: 8880 + protocol: TCP + - name: whip + containerPort: 8889 + protocol: TCP + - name: srt + containerPort: 8189 + protocol: UDP volumes: - name: cfg configMap: diff --git a/argos/deployments/deploy-minio.yaml b/argos/deployments/deploy-minio.yaml index 09a4f70..3c84773 100644 --- a/argos/deployments/deploy-minio.yaml +++ b/argos/deployments/deploy-minio.yaml @@ -4,23 +4,69 @@ metadata: name: minio namespace: argos-core spec: + strategy: + type: Recreate replicas: 1 - selector: { matchLabels: { app: minio } } + selector: + matchLabels: + app: minio template: - metadata: { labels: { app: minio } } + metadata: + labels: + app: minio + app.kubernetes.io/part-of: argos + app.kubernetes.io/managed-by: kustomize spec: + # ayuda a que el FS sea accesible por el grupo + securityContext: + fsGroup: 1000 + fsGroupChangePolicy: OnRootMismatch + # arregla permisos heredados de root en el PVC + initContainers: + - name: fix-perms + image: alpine:3.20 + command: ["/bin/sh","-c"] + args: + - | + set -ex + apk add --no-cache acl + chown -R 1000:1000 /data || true + chmod -R u+rwX,g+rwX /data || true + find /data -type d -exec chmod g+s {} \; || true + setfacl -R -m g:1000:rwx /data || true + setfacl -R -d -m g:1000:rwx /data || true + securityContext: + runAsUser: 0 + volumeMounts: + - name: data + mountPath: /data containers: - name: minio image: quay.io/minio/minio:latest + securityContext: + runAsUser: 1000 + runAsGroup: 1000 args: ["server", "/data", "--console-address", ":9001"] envFrom: - secretRef: { name: minio-creds } ports: - - containerPort: 9000 - - containerPort: 9001 - volumeMounts: - - name: data - mountPath: /data + - { containerPort: 9000, name: api } + - { containerPort: 9001, name: console } + readinessProbe: + httpGet: { path: /minio/health/ready, port: 9000 } + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: { path: /minio/health/live, port: 9000 } + initialDelaySeconds: 10 + periodSeconds: 20 + resources: + requests: + cpu: 50m + memory: 256Mi + limits: + cpu: "1" + memory: 2Gi volumes: - name: data persistentVolumeClaim: diff --git a/argos/deployments/deploy-mosquitto.yaml b/argos/deployments/deploy-mosquitto.yaml index a82eb87..75d1a94 100644 --- a/argos/deployments/deploy-mosquitto.yaml +++ b/argos/deployments/deploy-mosquitto.yaml @@ -5,22 +5,27 @@ metadata: namespace: argos-core spec: replicas: 1 - selector: { matchLabels: { app: mosquitto } } + selector: + matchLabels: { app: mosquitto } template: - metadata: { labels: { app: mosquitto } } + metadata: + labels: { app: mosquitto } spec: containers: - name: mosquitto - image: harbor.c2et.net/library/eclipse-mosquitto:latest + image: eclipse-mosquitto:2 ports: - containerPort: 1883 volumeMounts: - name: cfg mountPath: /mosquitto/config + - name: data + mountPath: /mosquitto/data volumes: - name: cfg configMap: - name: mosquitto-conf + name: mosquitto-config items: - - key: mosquitto.conf - path: mosquitto.conf + - { key: mosquitto.conf, path: mosquitto.conf } + - name: data + emptyDir: {} # o tu PVC si lo tienes diff --git a/argos/deployments/deploy-panel.yaml b/argos/deployments/deploy-panel.yaml index 900ccc8..8b0a99b 100644 --- a/argos/deployments/deploy-panel.yaml +++ b/argos/deployments/deploy-panel.yaml @@ -5,30 +5,30 @@ metadata: namespace: argos-core spec: replicas: 1 - selector: { matchLabels: { app: argos-panel } } + selector: + matchLabels: { app: argos-panel } template: - metadata: { labels: { app: argos-panel } } + metadata: + labels: { app: argos-panel } spec: containers: - name: panel - image: harbor.c2et.net/library/python:3.13.7-slim-bookworm + image: python:3.13.7-slim-bookworm command: ["/bin/sh","-c"] args: - | set -e pip install fastapi uvicorn minio - uvicorn app:app --host 0.0.0.0 --port 8000 + exec uvicorn app:app --host 0.0.0.0 --port 8000 --app-dir /app envFrom: - secretRef: { name: argos-panel-secret } volumeMounts: - - { name: app, mountPath: /app } - - { name: data, mountPath: /data } + - { name: app, mountPath: /app } ports: - - containerPort: 8000 + - { containerPort: 8000, name: http } volumes: - name: app configMap: name: argos-panel-config - items: [ { key: app.py, path: app.py } ] - - name: data - emptyDir: {} + items: + - { key: app.py, path: app.py } diff --git a/argos/ingress/ingress-panel.yaml b/argos/ingress/ingress-panel.yaml index d7095e2..192b844 100644 --- a/argos/ingress/ingress-panel.yaml +++ b/argos/ingress/ingress-panel.yaml @@ -3,16 +3,21 @@ kind: Ingress metadata: name: argos-panel-internal namespace: argos-core + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod spec: - ingressClassName: nginx-internal + ingressClassName: nginx rules: - - host: panel.argos.c2et.net # mismo FQDN + - host: argos.panel.c2et.net http: paths: - path: / pathType: Prefix backend: - service: { name: argos-panel, port: { number: 80 } } + service: + name: argos-panel + port: + name: http # <-- o usa "number: 8000" tls: - - hosts: ["panel.argos.c2et.net"] + - hosts: ["argos.panel.c2et.net"] secretName: panel-argos-tls diff --git a/argos/kustomization.yaml b/argos/kustomization.yaml index ce3752f..9c1edfa 100644 --- a/argos/kustomization.yaml +++ b/argos/kustomization.yaml @@ -3,14 +3,9 @@ kind: Kustomization namespace: argos-core -commonLabels: - app.kubernetes.io/part-of: argos - app.kubernetes.io/managed-by: kustomize - resources: - # Namespace y políticas + # Namespace - namespace.yaml - - policies/network-policy.yaml # ConfigMaps - configmaps/configmap-mediamtx.yaml @@ -18,31 +13,33 @@ resources: - configmaps/configmap-orchestrator.yaml - configmaps/configmap-panel.yaml - # Secrets - - secrets/secret-minio.yaml - - secrets/secret-orchestrator.yaml - - secrets/secret-panel.yaml - - # Storage - - pvc/pvc-minio.yaml - # Deployments - deployments/deploy-mediamtx.yaml + - deployments/deploy-minio.yaml - deployments/deploy-mosquitto.yaml - deployments/deploy-orchestrator.yaml - deployments/deploy-panel.yaml - - deployments/deploy-minio.yaml # Services - - services/argos-panel.yaml - services/svc-mediamtx-tcp.yaml - services/svc-mediamtx-udp.yaml - services/svc-minio.yaml - services/svc-mosquitto.yaml + - services/svc-panel.yaml # Ingress - ingress/ingress-minio.yaml - ingress/ingress-panel.yaml - # Certificados (si usas ACME en el externo y compartes el secret) - - certs/certificate-argos-panel.yaml + # Almacenamiento + - pvc/pvc-minio.yaml + + # Políticas de red + - policies/network-policy.yaml + - policies/allow-same-namespace.yaml + + # Secrets (si ya existen en el cluster, puedes omitirlos aquí) + - secrets/secret-minio.yaml + - secrets/secret-orchestrator.yaml + - secrets/secret-panel.yaml +# - secrets/secret-harbor-cred.yaml diff --git a/argos/policies/allow-same-namespace.yaml b/argos/policies/allow-same-namespace.yaml new file mode 100644 index 0000000..c30457c --- /dev/null +++ b/argos/policies/allow-same-namespace.yaml @@ -0,0 +1,11 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-same-namespace + namespace: argos-core +spec: + podSelector: {} # aplica a todos los pods de argos-core (destino) + policyTypes: [Ingress] + ingress: + - from: + - podSelector: {} # origen: cualquier pod del mismo namespace diff --git a/argos/readme.md b/argos/readme.md new file mode 100644 index 0000000..03313dc --- /dev/null +++ b/argos/readme.md @@ -0,0 +1,199 @@ +# Argos Core + +**Argos Core** es el backend central de una plataforma de videovigilancia distribuida. Está diseñado para recibir eventos desde **edges** (sitios remotos con Frigate), capturar/ingestar vídeo bajo demanda, almacenarlo de forma fiable y ofrecer un **panel web** seguro para consulta y reproducción, todo **encapsulado en Kubernetes** y oculto tras una **VPN WireGuard**. + +--- + +## Visión general + +```text +[Edge / Frigate] --(VPN/WG + MQTT eventos)--> [Argos Core] + ├─ Mosquitto (MQTT) + ├─ Orchestrator (ingesta clips) + │ ├─ Pull clip nativo de Frigate + │ └─ Fallback: captura RTSP temporal + ├─ MediaMTX (live/retransmisión) + ├─ MinIO (S3, almacenamiento) + └─ Panel (FastAPI + Uvicorn) +``` + +**Objetivo clave:** los edges **no** envían flujo constante; sólo cuando hay evento. El core ingesta y guarda el clip, y ofrece visualización **on‑demand**. + +--- + +## Componentes + +* **WireGuard (wg-easy)**: servidor VPN del clúster. Todo el tráfico de edges → core circula por WG. +* **Mosquitto** (`eclipse-mosquitto:2`): broker MQTT interno (ClusterIP). Topic de eventos (p. ej. `frigate/events`). +* **Orchestrator** (Python): + + * Se suscribe a MQTT. + * Tras un evento, intenta **descargar el clip nativo** desde la API del Frigate del edge. + * Si no existe, hace **captura RTSP** temporal con `ffmpeg` (duración configurada) contra la cámara/edge. + * Sube el resultado a **MinIO** (`s3://argos/...`) y registra metadatos en SQLite embebido. +* **MinIO** (S3 compatible): almacenamiento de clips y miniaturas. +* **MediaMTX**: redistribución de flujos (RTSP/WHIP/SRT) y posible live view on-demand. +* **Panel** (FastAPI/Uvicorn): + + * Lista eventos desde MinIO (sin exponer MinIO: **proxy** de objetos). + * Página de reproducción (`/view?key=…`). + * Endpoints simples (`/file`, `/api/events`, `/health`). +* **CoreDNS**: DNS interno con split-horizon (resolución diferente desde LAN/VPN/cluster cuando aplica). +* **Ingress NGINX (interno/externo)** + **cert‑manager**: TLS para el panel; HTTP‑01 servido por el controlador externo. +* **MetalLB**: IPs para servicios tipo LoadBalancer en la red `192.168.200.0/24` (p. ej. MediaMTX). + +--- + +## Flujo de datos (evento → clip) + +1. **Edge** detecta (Frigate) y publica en **MQTT** `frigate/events`. +2. **Orchestrator** recibe el evento, localiza la cámara/edge en su `settings.yaml`. +3. Intenta **pull** del **clip nativo** vía API Frigate del edge (VPN). +4. Si no hay clip, ejecuta **`ffmpeg`** para capturar **RTSP** temporal (fallback). +5. Sube el fichero a **MinIO** con una clave tipo: `camX/YYYY/MM/DD/TS_eventId.mp4` y optional thumb. +6. **Panel** lo muestra en la lista y permite reproducir vía `/file?key=…` (stream proxy desde MinIO). + +> Para **live**, MediaMTX publica `/` con `path=`; puedes incrustar un reproductor WebRTC/RTSP en el panel. + +--- + +## Despliegue + +### Requisitos previos + +* Kubernetes operativo con: + + * **MetalLB** (rango `192.168.200.0/24`). + * **NGINX Ingress** *interno* y *externo* (split-horizon DNS). + * **cert‑manager** + `ClusterIssuer` (p. ej. `letsencrypt-prod`). + * **wg-easy** funcionando (con NAT/iptables para alcanzar `200.x`, `0.x`, etc.). + +### Estructura + +``` +argos/ +├─ configmaps/ # mediamtx, mosquitto, orchestrator, panel +├─ deployments/ # mediamtx, minio, mosquitto, orchestrator, panel +├─ ingress/ # minio (opcional), panel (TLS) +├─ policies/ # network-policy, allow-same-namespace +├─ pvc/ # pvc-minio +├─ secrets/ # minio, orchestrator, panel +└─ services/ # mediamtx tcp/udp, minio, mosquitto, panel +``` + +### Variables/Secrets esperados + +* **MinIO** (`secrets/secret-minio.yaml`): `MINIO_ACCESS_KEY`, `MINIO_SECRET_KEY`. +* **Orchestrator** (`secrets/secret-orchestrator.yaml`): `MINIO_ENDPOINT`, `MINIO_ACCESS_KEY`, `MINIO_SECRET_KEY`, `MINIO_SECURE`. +* **Panel** (`secrets/secret-panel.yaml`): `MINIO_*` y `MINIO_BUCKET`. + +### Servicios y puertos + +* **MinIO**: `ClusterIP` :9000 (API), :9001 (console, opcional). No expuesto fuera. +* **Mosquitto**: `ClusterIP` :1883. +* **MediaMTX**: `LoadBalancer` (MetalLB) en `192.168.200.16` (RTSP 8554 TCP, WHIP 8889 TCP/HTTP 8880, SRT 8189 UDP). +* **Panel**: `ClusterIP` :8000 → Ingress (TLS) `https://argos.panel.c2et.net/` (ajusta FQDN). + +### Kustomize + +```bash +kubectl apply -k . +``` + +> Nota: evitamos `commonLabels` globales para no romper los **selectors**. + +--- + +## Configuración relevante + +### Orchestrator (`configmaps/configmap-orchestrator.yaml`) + +* `mqtt.host`: `mosquitto.argos-core.svc.cluster.local` +* `mqtt.port`: `1883` +* `mqtt.topic`: `frigate/events` +* `minio.endpoint`: `minio.argos-core.svc.cluster.local:9000` +* `edges`: lista de edges y cámaras (URLs de Frigate, RTSP principal, etc.). + +### Mosquitto (`configmaps/configmap-mosquitto.yaml`) + +* Config mínimo: + + ``` + persistence true + persistence_location /mosquitto/data/ + listener 1883 0.0.0.0 + allow_anonymous true # RECOMENDACIÓN: pasar a autenticación/TLS más adelante + ``` + +### MediaMTX + +* Ejecutar **sin** `/bin/sh -c …` y pasando **solo** la ruta del YAML como `args`. +* Corrige warnings como `sourceOnDemand` según el modo (`publisher` o `record`). + +### MinIO (seguridad/permisos) + +* Ejecuta como **no-root** con `runAsUser: 1000` y un **initContainer** que hace `chown/chmod/ACL` del PVC. +* Estrategia de despliegue **Recreate** para evitar doble pod en updates cuando la `readinessProbe` tarda. + +### Ingress + cert-manager + +* El **Certificate** del panel debe forzar el solver HTTP‑01 por el **ingress externo**: + + * Anotación: `acme.cert-manager.io/http01-ingress-class: nginx-external` (o ajusta en `ClusterIssuer`). + +--- + +## Operación + +### Comandos útiles + +```bash +# Estado general +kubectl -n argos-core get pods,svc,ingress,endpoints + +# Endpoints vacíos () +kubectl -n argos-core get endpoints -o wide +# Revisa selector del Service y labels del Pod + +# Logs +kubectl -n argos-core logs -f deploy/ + +# Reinicios controlados +kubectl -n argos-core rollout restart deploy/ + +# Diff del Service en vivo vs manifiesto +kubectl -n argos-core get svc argos-panel -o yaml --show-managed-fields=false +``` + +### Problemas típicos y fixes + +* **Service con `ENDPOINTS `**: selector ≠ labels, o pod **not Ready** (probe fallando). Ajusta selector a `{app: …}` y corrige readiness. +* **Dos pods tras update**: rolling update atascado. Usa `strategy: Recreate` mientras estabilizas. +* **MinIO `file access denied`**: permisos del PVC. Añade `initContainer` que hace `chown -R 1000:1000 /data` + ACL. +* **Mosquitto `Connection refused`** desde Orchestrator: Service sin endpoints o broker no escucha en 1883. Ver logs `mosquitto` y `ss -lnt`. +* **Panel `ASGI app not found`**: `uvicorn app:app --app-dir /app` y asegúrate de montar `app.py` con `app = FastAPI()`. +* **MediaMTX `unknown flag -c`**: no usar `/bin/sh -c …`; pasar `args: ["/config/mediamtx.yml"]`. + +--- + +## Edge (resumen de mañana) + +* **WireGuard** cliente (IP del edge en la VPN; DNS apuntando al core). +* **Frigate** en Docker + `mqtt.host=mosquitto.argos-core.svc.cluster.local`. +* API de Frigate accesible desde el core por la VPN (p. ej. `http://10.20.0.10:5000`). +* Opcional: MediaMTX local si se quiere “empujar” live al core durante la alarma. + +--- + +## Seguridad y siguientes pasos + +* Endurecer Mosquitto (usuarios/TLS), MinIO (políticas/buckets versionados), y el Panel (auth detrás de Ingress o login propio). +* Métricas y dashboards (Prometheus/Grafana), y mover metadatos a **PostgreSQL** para búsquedas ricas. +* Live WebRTC integrado en el panel (MediaMTX WHIP/WHEP). + +--- + +## Licencia / Créditos + +* **MediaMTX** (Bluenviron), **MinIO**, **Eclipse Mosquitto**, **Frigate** (en los edges). +* Argos Core: composición y orquestación Kubernetes + servicios auxiliares. diff --git a/argos/services/svc-mosquitto.yaml b/argos/services/svc-mosquitto.yaml index 72424c2..feffbeb 100644 --- a/argos/services/svc-mosquitto.yaml +++ b/argos/services/svc-mosquitto.yaml @@ -4,8 +4,7 @@ metadata: name: mosquitto namespace: argos-core spec: - type: LoadBalancer - loadBalancerIP: 192.168.200.15 + type: ClusterIP selector: { app: mosquitto } ports: - name: mqtt diff --git a/argos/services/argos-panel.yaml b/argos/services/svc-panel.yaml similarity index 72% rename from argos/services/argos-panel.yaml rename to argos/services/svc-panel.yaml index cc101e9..36da331 100644 --- a/argos/services/argos-panel.yaml +++ b/argos/services/svc-panel.yaml @@ -7,4 +7,6 @@ spec: type: ClusterIP selector: { app: argos-panel } ports: - - { name: http, port: 80, targetPort: 8000 } + - name: http + port: 8000 + targetPort: 8000 diff --git a/prueba/nuevoarchivo.md b/prueba/nuevoarchivo.md deleted file mode 100644 index af39bf8..0000000 --- a/prueba/nuevoarchivo.md +++ /dev/null @@ -1 +0,0 @@ -sdgfsdgsdfg \ No newline at end of file