chore: build
ci-api / test (push) Successful in 25m0s
ci-web / test (push) Successful in 15m18s

This commit is contained in:
Xor290
2026-08-19 15:05:08 +02:00
parent a278040545
commit f407bc3dd0
8 changed files with 102 additions and 30 deletions
@@ -813,8 +813,9 @@ func (h *HelmProvisioner) deleteNamespace(name string) error {
const veleroNamespace = "velero" const veleroNamespace = "velero"
var ( var (
veleroScheduleGVR = schema.GroupVersionResource{Group: "velero.io", Version: "v1", Resource: "schedules"} veleroScheduleGVR = schema.GroupVersionResource{Group: "velero.io", Version: "v1", Resource: "schedules"}
veleroBackupGVR = schema.GroupVersionResource{Group: "velero.io", Version: "v1", Resource: "backups"} veleroBackupGVR = schema.GroupVersionResource{Group: "velero.io", Version: "v1", Resource: "backups"}
veleroBackupRepoGVR = schema.GroupVersionResource{Group: "velero.io", Version: "v1", Resource: "backuprepositories"}
) )
// demoBackupScheduleName : nom du Schedule Velero dédié à ce namespace de // demoBackupScheduleName : nom du Schedule Velero dédié à ce namespace de
@@ -857,14 +858,24 @@ func (h *HelmProvisioner) ensureDemoBackupSchedule(namespace string) error {
return nil return nil
} }
// deleteDemoBackups supprime le Schedule Velero de cette démo ainsi que // deleteDemoBackups supprime le Schedule Velero de cette démo, toutes les
// toutes les sauvegardes déjà prises par ce planning (la suppression d'un // sauvegardes déjà prises par ce planning, et le BackupRepository associé
// Backup déclenche le nettoyage asynchrone des données Kopia associées côté // (la suppression d'un Backup déclenche le nettoyage asynchrone des données
// Velero — pas de suppression directe dans le bucket S3, qui risquerait de // Kopia associées côté Velero — pas de suppression directe dans le bucket
// casser le dépôt Kopia partagé par les autres démos). Appelée depuis // S3, qui risquerait de casser le dépôt Kopia partagé par les autres
// Teardown (expiration TTL et destruction forcée passent toutes les deux // démos). Appelée depuis Teardown (expiration TTL et destruction forcée
// par ce même chemin) et lors d'une migration premium réussie (l'ancien // passent toutes les deux par ce même chemin) et lors d'une migration
// namespace disparaît, son planning de sauvegarde n'a plus de sens). // premium réussie (l'ancien namespace disparaît, son planning de sauvegarde
// n'a plus de sens).
//
// Le BackupRepository (l'objet qui référence le dépôt Kopia du namespace,
// nommé "<namespace>-default-kopia") n'est PAS supprimé automatiquement par
// Velero quand ses Backup disparaissent — sans ce nettoyage explicite, il
// reste indéfiniment et Velero continue de lancer des jobs de maintenance
// périodiques dessus (kopia-maintain-job) pour un dépôt qui ne sert plus à
// rien. Supprimer l'objet ne touche pas aux données déjà écrites dans le
// bucket S3 (même logique que pour les Backup ci-dessus), juste le
// bookkeeping Velero.
func (h *HelmProvisioner) deleteDemoBackups(namespace string) error { func (h *HelmProvisioner) deleteDemoBackups(namespace string) error {
name := demoBackupScheduleName(namespace) name := demoBackupScheduleName(namespace)
@@ -886,6 +897,13 @@ func (h *HelmProvisioner) deleteDemoBackups(namespace string) error {
if err != nil && !k8sErrors.IsNotFound(err) { if err != nil && !k8sErrors.IsNotFound(err) {
return fmt.Errorf("suppression planning %s: %w", name, err) return fmt.Errorf("suppression planning %s: %w", name, err)
} }
repoName := namespace + "-default-kopia"
err = h.dynamicClient.Resource(veleroBackupRepoGVR).Namespace(veleroNamespace).
Delete(context.Background(), repoName, metav1.DeleteOptions{})
if err != nil && !k8sErrors.IsNotFound(err) {
return fmt.Errorf("suppression backuprepository %s: %w", repoName, err)
}
return nil return nil
} }
+16 -7
View File
@@ -1,8 +1,17 @@
# Identifiants à récupérer dans le Hub Beszel : Add System > Universal token. # Les nœuds k3s ne sont pas sur le VPN (voir omnex/ansible/inventory/host.ini)
# Le token universel expire au bout d'1h mais permet l'auto-enregistrement de # et le dashboard Beszel (8090) doit rester accessible uniquement via le VPN
# TOUS les agents du DaemonSet sans les déclarer un par un dans le Hub ; une # (10.0.0.0/24) — donc pas question de pointer HUB_URL dessus. Le Hub expose
# fois qu'un agent s'est enregistré, il garde son identité même après # à la place un port dédié 10002 (nginx, restreint aux 4 IP publiques des
# l'expiration du token (donc pas besoin de le renouveler après coup). # nœuds k3s-prod + chemin /api/ uniquement — voir
# ansible-mln/monitoring/security/nginx/nginx.conf et
# firewall-monitoring-uber.sh) : c'est CE port que les agents utilisent pour
# s'auto-enregistrer, jamais le dashboard.
#
# TOKEN : token universel généré dans le Hub (Add System > Universal token,
# expire au bout d'1h mais un agent déjà enregistré garde son identité).
# KEY : clé publique SSH du Hub (une seule pour tous les agents, quel que
# soit le système) — même valeur que celle déjà utilisée par l'agent
# s3-uber (voir ansible-mln/monitoring/s3-backup/docker-compose.yml).
apiVersion: v1 apiVersion: v1
kind: Secret kind: Secret
metadata: metadata:
@@ -10,6 +19,6 @@ metadata:
namespace: monitoring namespace: monitoring
type: Opaque type: Opaque
stringData: stringData:
HUB_URL: "" # ex: https://beszel.exemple.com HUB_URL: "https://185.103.167.138:10002"
TOKEN: "" # token universel généré dans le Hub TOKEN: "" # token universel généré dans le Hub
KEY: "" # clé publique SSH affichée par le Hub KEY: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEwRVn66crRDX5iL0bxNOnrp3OhTiXHKs+COx/bo5vmQ beszel-agent"
+8 -6
View File
@@ -1,9 +1,11 @@
# Isolation réseau du namespace monitoring (beszel-agent) — deny-by-default + # Isolation réseau du namespace monitoring (beszel-agent) — deny-by-default +
# allowlist : # allowlist :
# - Egress : HTTPS sortant vers le Hub (HUB_URL, hôte externe arbitraire — # - Egress : HTTPS sortant vers le Hub sur son port dédié agents (10002,
# voir beszel-agent-secret.yml) pour l'auto-enregistrement au démarrage. # pas 8090/dashboard — voir beszel-agent-secret.yml et
# ansible-mln/monitoring/security/nginx/nginx.conf) pour
# l'auto-enregistrement au démarrage.
# - Ingress : le Hub se connecte à l'agent en SSH sur le port LISTEN # - Ingress : le Hub se connecte à l'agent en SSH sur le port LISTEN
# (45876) pour récupérer les métriques — IP du Hub non fixe/connue # (10002) pour récupérer les métriques — IP du Hub non fixe/connue
# d'avance, ouvert largement mais authentifié par TOKEN/KEY (voir # d'avance, ouvert largement mais authentifié par TOKEN/KEY (voir
# beszel-agent-secret.yml), pas par l'origine réseau. # beszel-agent-secret.yml), pas par l'origine réseau.
# #
@@ -14,7 +16,7 @@
# GlobalNetworkPolicy Calico, une ressource différente, pour un enforcement # GlobalNetworkPolicy Calico, une ressource différente, pour un enforcement
# réel au niveau du nœud). Ce manifeste reste appliqué pour la correction # réel au niveau du nœud). Ce manifeste reste appliqué pour la correction
# d'intention et si hostNetwork est retiré un jour, mais ne pas compter # d'intention et si hostNetwork est retiré un jour, mais ne pas compter
# dessus comme protection effective du port 45876 en l'état — un pare-feu # dessus comme protection effective du port 10002 en l'état — un pare-feu
# hôte (iptables/nftables/ufw) ou une GlobalNetworkPolicy Calico est # hôte (iptables/nftables/ufw) ou une GlobalNetworkPolicy Calico est
# nécessaire pour une vraie restriction. # nécessaire pour une vraie restriction.
@@ -43,7 +45,7 @@ spec:
- from: [] - from: []
ports: ports:
- protocol: TCP - protocol: TCP
port: 45876 port: 10002
--- ---
apiVersion: networking.k8s.io/v1 apiVersion: networking.k8s.io/v1
@@ -86,4 +88,4 @@ spec:
- 192.168.0.0/16 - 192.168.0.0/16
ports: ports:
- protocol: TCP - protocol: TCP
port: 443 port: 10002
@@ -83,6 +83,13 @@ data:
# mécanisme que docker/waf (omnex-prod). # mécanisme que docker/waf (omnex-prod).
SecAuditLogParts ABIFHZ SecAuditLogParts ABIFHZ
# 911100 (REQUEST-911-METHOD-ENFORCEMENT) n'autorise par défaut que GET
# HEAD POST OPTIONS — bloque tout DELETE/PUT/PATCH légitime de l'API
# REST. Même correctif appliqué côté docker/waf/custom-rules.conf
# (omnex-prod) et côté Coraza/Traefik (deploy/chart-gestion/traefik/
# templates/middleware-waf.yaml).
SecRuleRemoveById 911100
SecRule IP:BANNED "@eq 1" \ SecRule IP:BANNED "@eq 1" \
"id:100000,phase:1,deny,status:403,log,\ "id:100000,phase:1,deny,status:403,log,\
msg:'IP is banned'" msg:'IP is banned'"
+1 -1
View File
@@ -45,7 +45,7 @@ spec:
name: beszel-agent-hub name: beszel-agent-hub
env: env:
- name: LISTEN - name: LISTEN
value: "45876" value: "10002"
- name: SYSTEM_NAME - name: SYSTEM_NAME
valueFrom: valueFrom:
fieldRef: fieldRef:
+36 -4
View File
@@ -137,6 +137,36 @@ roleRef:
kind: ClusterRole kind: ClusterRole
name: omnex-namespace-manager name: omnex-namespace-manager
--- ---
# "nodes" n'est pas un type namespaced : impossible de le scoper via un
# RoleBinding par démo comme omnex-demo-workload-manager, d'où une
# ClusterRole/ClusterRoleBinding dédiée. Portée strictement à "get" sur le
# sous-type "nodes/proxy" — c'est tout ce dont checkPVCUsage a besoin
# (voir alerts/resources.go:fetchNodeSummary, équivalent de
# `kubectl get --raw /api/v1/nodes/<node>/proxy/stats/summary`) pour lire
# le remplissage des PVC. Aucun accès en écriture, aucun accès aux autres
# sous-ressources de Node.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: omnex-node-stats-reader
rules:
- apiGroups: [""]
resources: [nodes/proxy]
verbs: [get]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: omnex-control-plane-node-stats
subjects:
- kind: ServiceAccount
name: omnex-control-plane
namespace: omnex-system
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: omnex-node-stats-reader
---
apiVersion: rbac.authorization.k8s.io/v1 apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding kind: ClusterRoleBinding
metadata: metadata:
@@ -166,8 +196,8 @@ type: kubernetes.io/service-account-token
# namespace fixe préexistant, pas un namespace de démo créé dynamiquement # namespace fixe préexistant, pas un namespace de démo créé dynamiquement
# par le control-plane — pas besoin du mécanisme "bind" utilisé pour # par le control-plane — pas besoin du mécanisme "bind" utilisé pour
# omnex-demo-workload-manager). Portée strictement limitée aux Schedule/ # omnex-demo-workload-manager). Portée strictement limitée aux Schedule/
# Backup Velero — aucun accès aux Secrets (dont cloud-credentials, la clé # Backup/BackupRepository Velero — aucun accès aux Secrets (dont
# du bucket S3) ni au reste du namespace velero. # cloud-credentials, la clé du bucket S3) ni au reste du namespace velero.
apiVersion: rbac.authorization.k8s.io/v1 apiVersion: rbac.authorization.k8s.io/v1
kind: Role kind: Role
metadata: metadata:
@@ -178,8 +208,10 @@ rules:
# Un Schedule + les Backup qu'il crée par démo (voir # Un Schedule + les Backup qu'il crée par démo (voir
# HelmProvisioner.ensureDemoBackupSchedule/deleteDemoBackups) — jamais # HelmProvisioner.ensureDemoBackupSchedule/deleteDemoBackups) — jamais
# de wildcard multi-namespace, pour pouvoir supprimer les sauvegardes # de wildcard multi-namespace, pour pouvoir supprimer les sauvegardes
# d'UNE démo sans toucher aux autres. # d'UNE démo sans toucher aux autres. backuprepositories : nettoyage du
resources: [schedules, backups] # dépôt Kopia du namespace détruit (deleteDemoBackups) — sans quoi
# Velero continue de lancer des jobs de maintenance dessus indéfiniment.
resources: [schedules, backups, backuprepositories]
verbs: [get, list, watch, create, delete] verbs: [get, list, watch, create, delete]
--- ---
apiVersion: rbac.authorization.k8s.io/v1 apiVersion: rbac.authorization.k8s.io/v1
+3 -1
View File
@@ -61,7 +61,9 @@ export function Demos() {
// Les démos passées en abonnement payant vivent désormais dans le // Les démos passées en abonnement payant vivent désormais dans le
// dashboard Premium (n'expirent plus, stockage persistant) — ne plus // dashboard Premium (n'expirent plus, stockage persistant) — ne plus
// les mélanger avec les démos d'essai ici. // les mélanger avec les démos d'essai ici.
setDemos((res.items ?? []).filter((d) => d.type_abonnement !== 'premium')) setDemos(
(res.items ?? []).filter((d) => d.type_abonnement !== 'premium' && d.status !== 'expired'),
)
} catch (err) { } catch (err) {
if (err instanceof ApiError && err.status === 401) navigate('/admin/login') if (err instanceof ApiError && err.status === 401) navigate('/admin/login')
else toast({ status: 'error', title: 'Chargement des démos impossible' }) else toast({ status: 'error', title: 'Chargement des démos impossible' })
+3 -1
View File
@@ -56,7 +56,9 @@ export function PremiumDemos() {
const load = useCallback(async () => { const load = useCallback(async () => {
try { try {
const res = await api.listDemos() const res = await api.listDemos()
setDemos((res.items ?? []).filter((d) => d.type_abonnement === 'premium')) setDemos(
(res.items ?? []).filter((d) => d.type_abonnement === 'premium' && d.status !== 'expired'),
)
} catch (err) { } catch (err) {
if (err instanceof ApiError && err.status === 401) navigate('/admin/login') if (err instanceof ApiError && err.status === 401) navigate('/admin/login')
else toast({ status: 'error', title: 'Chargement des démos impossible' }) else toast({ status: 'error', title: 'Chargement des démos impossible' })