añadido gitea y medio argos

This commit is contained in:
2025-08-18 10:29:04 +02:00
parent f37ed27613
commit 8e09b2e33e
40 changed files with 929 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
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

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: mediamtx-config
namespace: argos-core
data:
mediamtx.yml: |
logLevel: info
rtsp: yes
rtmp: no
hls: no
webrtc: yes
paths:
all:
sourceOnDemand: yes

View File

@@ -0,0 +1,9 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: mosquitto-conf
namespace: argos-core
data:
mosquitto.conf: |
listener 1883 0.0.0.0
allow_anonymous true

View File

@@ -0,0 +1,156 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: argos-orchestrator-config
namespace: argos-core
data:
settings.yaml: |
mqtt:
host: mqtt.argos.interna
port: 1883
topic: frigate/events
client_id: argos-core
minio:
bucket: argos
region: us-east-1
edges:
- name: rpi01
base_url: http://10.20.0.10:5000 # URL del Frigate del edge (VPN). 5000 por defecto.
api_token: "" # si usas auth, ponlo aquí
cameras:
- name: cam1
rtsp_main: rtsp://10.20.0.10:8554/cam1_main
path_mtx: cam1 # para abrir live en MediaMTX: /?path=cam1
app.py: |
import os, json, time, datetime, sqlite3, subprocess, yaml, requests
from pathlib import Path
from urllib.parse import urljoin
import paho.mqtt.client as mqtt
from minio import Minio
CFG_PATH="/app/settings.yaml"
DB_PATH="/data/argos.db"
TMP="/tmp"
with open(CFG_PATH,"r") as f:
CFG=yaml.safe_load(f)
# Map quick-lookup: camera -> (edge, cfg)
CAM_MAP={}
for e in CFG.get("edges", []):
for c in e.get("cameras", []):
CAM_MAP[c["name"]]=(e, c)
# MinIO
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")
BUCKET=CFG["minio"]["bucket"]
if not mc.bucket_exists(BUCKET): mc.make_bucket(BUCKET)
# DB
Path("/data").mkdir(parents=True, exist_ok=True)
con=sqlite3.connect(DB_PATH, check_same_thread=False)
cur=con.cursor()
cur.execute("""CREATE TABLE IF NOT EXISTS events(
id TEXT PRIMARY KEY, ts INTEGER, edge TEXT, camera TEXT, label TEXT,
s3url TEXT, thumb_s3 TEXT
)""")
con.commit()
def upload_file(local_path, key, content_type):
mc.fput_object(BUCKET, key, local_path, content_type=content_type)
return f"s3://{BUCKET}/{key}"
def fetch_frigate_clip(edge_cfg, ev_id):
""" Descarga el clip nativo de Frigate si existe """
base=edge_cfg["base_url"]
url=urljoin(base, f"/api/events/{ev_id}/clip")
headers={}
if edge_cfg.get("api_token"):
headers["Authorization"]=f"Bearer {edge_cfg['api_token']}"
r=requests.get(url, headers=headers, stream=True, timeout=30)
if r.status_code!=200: return None
tmp=f"{TMP}/{ev_id}.mp4"
with open(tmp,"wb") as f:
for chunk in r.iter_content(1<<20):
if chunk: f.write(chunk)
return tmp
def fetch_frigate_thumb(edge_cfg, ev_id):
base=edge_cfg["base_url"]
url=urljoin(base, f"/api/events/{ev_id}/thumbnail.jpg")
headers={}
if edge_cfg.get("api_token"):
headers["Authorization"]=f"Bearer {edge_cfg['api_token']}"
r=requests.get(url, headers=headers, timeout=10)
if r.status_code!=200: return None
tmp=f"{TMP}/{ev_id}.jpg"
with open(tmp,"wb") as f: f.write(r.content)
return tmp
def record_rtsp(rtsp_url, seconds=30):
tmp=f"{TMP}/rtsp_{int(time.time())}.mp4"
cmd=["ffmpeg","-nostdin","-y","-rtsp_transport","tcp","-i",rtsp_url,"-t",str(seconds),"-c","copy",tmp]
try:
subprocess.run(cmd, check=True)
return tmp
except Exception as e:
print("FFmpeg fallback failed:", e)
return None
def on_message(client, userdata, msg):
try:
payload=json.loads(msg.payload.decode("utf-8"))
except Exception as e:
print("Bad JSON", e); return
ev_type=payload.get("type")
after=payload.get("after") or {}
ev_id=after.get("id") or payload.get("id")
cam=after.get("camera") or payload.get("camera")
label=after.get("label") or payload.get("label","")
if ev_type!="new" or not cam or not ev_id: return
if cam not in CAM_MAP:
print("Unknown camera:", cam); return
edge_cfg, cam_cfg = CAM_MAP[cam]
ts=int(time.time())
print(f"[ARGOS] {ev_id} {cam} → try Frigate clip")
path_local = fetch_frigate_clip(edge_cfg, ev_id)
if not path_local:
print(f"[ARGOS] {ev_id} no native clip, fallback RTSP")
path_local = record_rtsp(cam_cfg["rtsp_main"], seconds=30)
if not path_local:
print(f"[ARGOS] {ev_id} failed recording"); return
# upload clip
date=datetime.datetime.utcfromtimestamp(ts)
key=f"{cam}/{date.year:04d}/{date.month:02d}/{date.day:02d}/{ts}_{ev_id}.mp4"
s3url=upload_file(path_local, key, "video/mp4")
try: os.remove(path_local)
except: pass
# thumbnail
thumb_local = fetch_frigate_thumb(edge_cfg, ev_id)
thumb_s3=None
if thumb_local:
tkey=f"{cam}/thumbs/{ts}_{ev_id}.jpg"
thumb_s3=upload_file(thumb_local, tkey, "image/jpeg")
try: os.remove(thumb_local)
except: pass
cur.execute("INSERT OR REPLACE INTO events(id, ts, edge, camera, label, s3url, thumb_s3) VALUES (?,?,?,?,?,?,?)",
(ev_id, ts, edge_cfg["name"], cam, label, s3url, thumb_s3))
con.commit()
print(f"[ARGOS] stored {s3url}")
def main():
m=mqtt.Client(client_id=CFG["mqtt"]["client_id"], clean_session=True)
m.connect(CFG["mqtt"]["host"], CFG["mqtt"]["port"], keepalive=60)
m.subscribe(CFG["mqtt"]["topic"])
m.on_message=on_message
m.loop_forever()
if __name__=="__main__": main()

View File

@@ -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 """
<!doctype html><meta charset="utf-8"><title>ARGOS Panel</title>
<style>body{font-family:system-ui;margin:1.5rem} .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px}
.card{border:1px solid #ddd;border-radius:10px;padding:10px} img{width:100%;height:160px;object-fit:cover;border-radius:8px}
button{padding:.4rem .6rem;margin-right:.3rem}</style>
<h1>ARGOS Alarmas</h1>
<div class="grid" id="grid"></div>
<div style="margin-top:1rem"><video id="player" width="960" controls></video></div>
<script>
const fmt=t=>new Date(t*1000).toLocaleString();
async function load(){
const r=await fetch('/api/events?limit=100'); const data=await r.json();
const g=document.getElementById('grid'); g.innerHTML='';
for(const ev of data){
const d=document.createElement('div'); d.className='card';
const img = ev.thumb ? `<img src="${ev.thumb.replace('s3://','/api/url/THUMB?key=')}" alt="thumb">` : '';
d.innerHTML = `${img}<div><b>${ev.camera}</b> — ${fmt(ev.ts)}<br>${ev.label||''}</div>
<div style="margin-top:.4rem">
<button data-id="${ev.id}" data-action="clip">Ver clip</button>
<button data-path="${ev.camera}" data-action="live">En directo</button>
</div>`;
g.appendChild(d);
}
g.onclick=async (e)=>{
if(e.target.tagName!=='BUTTON') return;
const v=document.getElementById('player');
if(e.target.dataset.action==='clip'){
const id=e.target.dataset.id;
const j=await (await fetch('/api/url/'+id)).json();
v.src=j.url; v.play();
}else if(e.target.dataset.action==='live'){
const path=e.target.dataset.path;
// usa MediaMTX web player
window.open('http://mediamtx.argos.interna/?path='+encodeURIComponent(path),'_blank');
}
}
}
load(); setInterval(load,10000);
</script>
"""

View File

@@ -0,0 +1,34 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: mediamtx
namespace: argos-core
spec:
replicas: 1
selector: { matchLabels: { app: mediamtx } }
template:
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
volumeMounts:
- name: cfg
mountPath: /config
ports:
- containerPort: 8554 # RTSP
- containerPort: 8189 # SRT
- containerPort: 8889 # WHIP
- containerPort: 8880 # HTTP/API
volumes:
- name: cfg
configMap:
name: mediamtx-config

View File

@@ -0,0 +1,27 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: minio
namespace: argos-core
spec:
replicas: 1
selector: { matchLabels: { app: minio } }
template:
metadata: { labels: { app: minio } }
spec:
containers:
- name: minio
image: quay.io/minio/minio:latest
args: ["server", "/data", "--console-address", ":9001"]
envFrom:
- secretRef: { name: minio-creds }
ports:
- containerPort: 9000
- containerPort: 9001
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: minio-data

View File

@@ -0,0 +1,26 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: mosquitto
namespace: argos-core
spec:
replicas: 1
selector: { matchLabels: { app: mosquitto } }
template:
metadata: { labels: { app: mosquitto } }
spec:
containers:
- name: mosquitto
image: harbor.c2et.net/library/eclipse-mosquitto:latest
ports:
- containerPort: 1883
volumeMounts:
- name: cfg
mountPath: /mosquitto/config
volumes:
- name: cfg
configMap:
name: mosquitto-conf
items:
- key: mosquitto.conf
path: mosquitto.conf

View File

@@ -0,0 +1,35 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: argos-orchestrator
namespace: argos-core
spec:
replicas: 1
selector: { matchLabels: { app: argos-orchestrator } }
template:
metadata: { labels: { app: argos-orchestrator } }
spec:
containers:
- name: orchestrator
image: harbor.c2et.net/library/python:3.13.7-slim-bookworm
command: ["/bin/sh","-c"]
args:
- |
set -e
apt-get update && apt-get install -y --no-install-recommends ffmpeg curl && rm -rf /var/lib/apt/lists/*
pip install paho-mqtt minio pyyaml requests
python /app/app.py
envFrom:
- secretRef: { name: argos-orchestrator-secret }
volumeMounts:
- { name: cfg, mountPath: /app }
- { name: data, mountPath: /data }
volumes:
- name: cfg
configMap:
name: argos-orchestrator-config
items:
- { key: app.py, path: app.py }
- { key: settings.yaml, path: settings.yaml }
- name: data
emptyDir: {}

View File

@@ -0,0 +1,34 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: argos-panel
namespace: argos-core
spec:
replicas: 1
selector: { matchLabels: { app: argos-panel } }
template:
metadata: { labels: { app: argos-panel } }
spec:
containers:
- name: panel
image: docker.io/library/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
envFrom:
- secretRef: { name: argos-panel-secret }
volumeMounts:
- { name: app, mountPath: /app }
- { name: data, mountPath: /data }
ports:
- containerPort: 8000
volumes:
- name: app
configMap:
name: argos-panel-config
items: [ { key: app.py, path: app.py } ]
- name: data
emptyDir: {}

View File

@@ -0,0 +1,26 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: minio-ingress
namespace: argos-core
spec:
ingressClassName: nginx
rules:
- host: s3.argos.interna
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: minio
port: { number: 9000 }
- host: minio.argos.interna
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: minio
port: { number: 9001 }

View File

@@ -0,0 +1,18 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: argos-panel-internal
namespace: argos-core
spec:
ingressClassName: nginx-internal
rules:
- host: panel.argos.c2et.net # mismo FQDN
http:
paths:
- path: /
pathType: Prefix
backend:
service: { name: argos-panel, port: { number: 80 } }
tls:
- hosts: ["panel.argos.c2et.net"]
secretName: panel-argos-tls

48
argos/kustomization.yaml Normal file
View File

@@ -0,0 +1,48 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: argos-core
commonLabels:
app.kubernetes.io/part-of: argos
app.kubernetes.io/managed-by: kustomize
resources:
# Namespace y políticas
- namespace.yaml
- policies/network-policy.yaml
# ConfigMaps
- configmaps/configmap-mediamtx.yaml
- configmaps/configmap-mosquitto.yaml
- 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-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
# 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

4
argos/namespace.yaml Normal file
View File

@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: argos-core

View File

@@ -0,0 +1,12 @@
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-from-wg-and-200
namespace: argos-core
spec:
podSelector: {}
policyTypes: [Ingress]
ingress:
- from:
- ipBlock: { cidr: 192.168.254.0/24 } # WireGuard peers
- ipBlock: { cidr: 192.168.200.0/24 } # red 200 (acceso interno/admin)

11
argos/pvc/pvc-minio.yaml Normal file
View File

@@ -0,0 +1,11 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: minio-data
namespace: argos-core
spec:
storageClassName: ceph-rbd
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 1Ti # ajusta capacidad

View File

@@ -0,0 +1,9 @@
apiVersion: v1
data:
.dockerconfigjson: eyJhdXRocyI6eyJoYXJib3IuYzJldC5jb20iOnsidXNlcm5hbWUiOiJ4YXZvciIsInBhc3N3b3JkIjoiTUBuYWJvMjAyNSIsImVtYWlsIjoibm8tcmVwbHlAYzJldC5jb20iLCJhdXRoIjoiZUdGMmIzSTZUVUJ1WVdKdk1qQXlOUT09In19fQ==
kind: Secret
metadata:
creationTimestamp: null
name: harbor-cred
namespace: apolo
type: kubernetes.io/dockerconfigjson

View File

@@ -0,0 +1,9 @@
apiVersion: v1
kind: Secret
metadata:
name: minio-creds
namespace: argos-core
type: Opaque
stringData:
MINIO_ROOT_USER: admin
MINIO_ROOT_PASSWORD: adminadmin123

View File

@@ -0,0 +1,11 @@
apiVersion: v1
kind: Secret
metadata:
name: argos-orchestrator-secret
namespace: argos-core
type: Opaque
stringData:
MINIO_ENDPOINT: minio.argos-core.svc.cluster.local:9000
MINIO_ACCESS_KEY: admin
MINIO_SECRET_KEY: adminadmin123
MINIO_SECURE: "false"

View File

@@ -0,0 +1,11 @@
apiVersion: v1
kind: Secret
metadata:
name: argos-panel-secret
namespace: argos-core
type: Opaque
stringData:
MINIO_ENDPOINT: minio.argos-core.svc.cluster.local:9000
MINIO_ACCESS_KEY: admin
MINIO_SECRET_KEY: adminadmin123
MINIO_SECURE: "false"

View File

@@ -0,0 +1,10 @@
apiVersion: v1
kind: Service
metadata:
name: argos-panel
namespace: argos-core
spec:
type: ClusterIP
selector: { app: argos-panel }
ports:
- { name: http, port: 80, targetPort: 8000 }

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: mediamtx-tcp
namespace: argos-core
annotations:
metallb.universe.tf/allow-shared-ip: "mediamtx-addr"
spec:
type: LoadBalancer
loadBalancerIP: 192.168.200.16
selector: { app: mediamtx }
ports:
- { name: rtsp, port: 8554, targetPort: 8554, protocol: TCP }
- { name: http, port: 8880, targetPort: 8880, protocol: TCP }
- { name: whip, port: 8889, targetPort: 8889, protocol: TCP }

View File

@@ -0,0 +1,13 @@
apiVersion: v1
kind: Service
metadata:
name: mediamtx-udp
namespace: argos-core
annotations:
metallb.universe.tf/allow-shared-ip: "mediamtx-addr"
spec:
type: LoadBalancer
loadBalancerIP: 192.168.200.16
selector: { app: mediamtx }
ports:
- { name: srt, port: 8189, targetPort: 8189, protocol: UDP }

View File

@@ -0,0 +1,11 @@
apiVersion: v1
kind: Service
metadata:
name: minio
namespace: argos-core
spec:
type: ClusterIP
selector: { app: minio }
ports:
- { name: api, port: 9000, targetPort: 9000, protocol: TCP }
- { name: console, port: 9001, targetPort: 9001, protocol: TCP }

View File

@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: mosquitto
namespace: argos-core
spec:
type: LoadBalancer
loadBalancerIP: 192.168.200.15
selector: { app: mosquitto }
ports:
- name: mqtt
port: 1883
targetPort: 1883
protocol: TCP