189 lines
6.0 KiB
Go
189 lines
6.0 KiB
Go
package alerts
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"time"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/client-go/kubernetes"
|
|
metricsclient "k8s.io/metrics/pkg/client/clientset/versioned"
|
|
)
|
|
|
|
const pollInterval = 30 * time.Second
|
|
|
|
// Raisons de conteneur considérées comme des pannes (hors CrashLoopBackOff
|
|
// initial normal au tout premier démarrage d'un pod fraîchement créé — non
|
|
// distingué ici, le bruit au provisioning est acceptable).
|
|
var badWaitingReasons = map[string]bool{
|
|
"CrashLoopBackOff": true,
|
|
"ImagePullBackOff": true,
|
|
"ErrImagePull": true,
|
|
"CreateContainerConfigError": true,
|
|
"CreateContainerError": true,
|
|
"InvalidImageName": true,
|
|
"RunContainerError": true,
|
|
}
|
|
|
|
type podKey struct{ namespace, pod string }
|
|
|
|
// Watcher scrute périodiquement les pods des namespaces actifs et notifie
|
|
// tout changement d'état (sain -> panne, panne -> rétabli), ainsi que les
|
|
// dépassements de seuil CPU/mémoire (voir resources.go) et de remplissage
|
|
// des volumes (PVC). Les destinataires (recipients) sont résolus à chaque
|
|
// tick où une notification est nécessaire — chaque admin configure son
|
|
// propre webhook Discord / bot Telegram dans son profil (voir
|
|
// internal/profile), rien n'est figé au démarrage.
|
|
type Watcher struct {
|
|
k8sClient *kubernetes.Clientset
|
|
metricsClient *metricsclient.Clientset // optionnel : nil désactive les alertes CPU/mémoire (le reste du watcher continue de fonctionner)
|
|
recipients func(ctx context.Context) ([]Notifier, error)
|
|
namespaces func() ([]string, error)
|
|
state map[podKey]string // dernière raison de panne connue ("" = sain)
|
|
resourceHigh map[podKey]int // ticks consécutifs au-dessus du seuil CPU/mémoire
|
|
pvcHigh map[pvcKey]int // ticks consécutifs au-dessus du seuil de remplissage PVC
|
|
}
|
|
|
|
func NewWatcher(k8sClient *kubernetes.Clientset, metricsClient *metricsclient.Clientset, recipients func(ctx context.Context) ([]Notifier, error), namespaces func() ([]string, error)) *Watcher {
|
|
return &Watcher{
|
|
k8sClient: k8sClient,
|
|
metricsClient: metricsClient,
|
|
recipients: recipients,
|
|
namespaces: namespaces,
|
|
state: make(map[podKey]string),
|
|
resourceHigh: make(map[podKey]int),
|
|
pvcHigh: make(map[pvcKey]int),
|
|
}
|
|
}
|
|
|
|
// Run boucle jusqu'à annulation du contexte. À lancer dans une goroutine.
|
|
func (w *Watcher) Run(ctx context.Context) {
|
|
ticker := time.NewTicker(pollInterval)
|
|
defer ticker.Stop()
|
|
for {
|
|
w.tick(ctx)
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
}
|
|
}
|
|
}
|
|
|
|
func (w *Watcher) tick(ctx context.Context) {
|
|
nsList, err := w.namespaces()
|
|
if err != nil {
|
|
log.Printf("alerts: liste des démos actives indisponible: %v", err)
|
|
return
|
|
}
|
|
nsSet := make(map[string]bool, len(nsList))
|
|
for _, ns := range nsList {
|
|
nsSet[ns] = true
|
|
}
|
|
|
|
// Les destinataires ne sont chargés (requête DB) que si une notification
|
|
// s'avère nécessaire ce tick-ci, et une seule fois par tick.
|
|
var notifier Notifier
|
|
notifierLoaded := false
|
|
loadNotifier := func() Notifier {
|
|
if notifierLoaded {
|
|
return notifier
|
|
}
|
|
notifierLoaded = true
|
|
list, err := w.recipients(ctx)
|
|
if err != nil {
|
|
log.Printf("alerts: chargement des destinataires échoué: %v", err)
|
|
return nil
|
|
}
|
|
if len(list) == 0 {
|
|
return nil
|
|
}
|
|
notifier = MultiNotifier(list)
|
|
return notifier
|
|
}
|
|
|
|
seen := make(map[podKey]bool, len(w.state))
|
|
seenHigh := make(map[podKey]bool, len(w.resourceHigh))
|
|
nodeNames := make(map[string]bool) // nœuds hébergeant un pod Running d'un namespace actif (voir checkPVCUsage)
|
|
|
|
for _, ns := range nsList {
|
|
pods, err := w.k8sClient.CoreV1().Pods(ns).List(ctx, metav1.ListOptions{})
|
|
if err != nil {
|
|
log.Printf("alerts: liste des pods de %s indisponible: %v", ns, err)
|
|
continue
|
|
}
|
|
for _, pod := range pods.Items {
|
|
key := podKey{namespace: ns, pod: pod.Name}
|
|
seen[key] = true
|
|
|
|
reason := problemReason(pod)
|
|
prev, known := w.state[key]
|
|
if reason != prev {
|
|
if known {
|
|
w.notify(ctx, loadNotifier(), key, prev, reason)
|
|
} else if reason != "" {
|
|
// Premier passage sur ce pod : on ne notifie que s'il est
|
|
// déjà en panne (sinon rien d'anormal à signaler).
|
|
w.notify(ctx, loadNotifier(), key, "", reason)
|
|
}
|
|
w.state[key] = reason
|
|
}
|
|
|
|
if pod.Status.Phase == corev1.PodRunning && pod.Spec.NodeName != "" {
|
|
nodeNames[pod.Spec.NodeName] = true
|
|
}
|
|
}
|
|
|
|
w.checkResourceUsage(ctx, loadNotifier, ns, pods.Items, seenHigh)
|
|
}
|
|
|
|
// Pods disparus (démo supprimée, rollout) : on oublie leur état pour ne
|
|
// pas polluer la mémoire indéfiniment.
|
|
for key := range w.state {
|
|
if !seen[key] {
|
|
delete(w.state, key)
|
|
}
|
|
}
|
|
w.clearResourceRecovered(ctx, loadNotifier, seenHigh)
|
|
w.checkPVCUsage(ctx, loadNotifier, nsSet, nodeNames)
|
|
}
|
|
|
|
func (w *Watcher) notify(ctx context.Context, notifier Notifier, key podKey, prev, reason string) {
|
|
var msg string
|
|
switch {
|
|
case prev == "" && reason != "":
|
|
msg = fmt.Sprintf("🔴 [%s] pod %s en erreur : %s", key.namespace, key.pod, reason)
|
|
case prev != "" && reason == "":
|
|
msg = fmt.Sprintf("🟢 [%s] pod %s rétabli", key.namespace, key.pod)
|
|
default:
|
|
msg = fmt.Sprintf("🔴 [%s] pod %s toujours en erreur : %s", key.namespace, key.pod, reason)
|
|
}
|
|
w.send(ctx, notifier, msg)
|
|
}
|
|
|
|
// problemReason retourne une raison de panne non vide si le pod est dans un
|
|
// état anormal (phase Failed, ou conteneur en CrashLoopBackOff/ImagePullBackOff/
|
|
// OOMKilled...), "" si tout va bien.
|
|
func problemReason(pod corev1.Pod) string {
|
|
if pod.DeletionTimestamp != nil {
|
|
return "" // suppression en cours, pas une panne
|
|
}
|
|
if pod.Status.Phase == corev1.PodFailed {
|
|
if pod.Status.Reason != "" {
|
|
return pod.Status.Reason
|
|
}
|
|
return "Failed"
|
|
}
|
|
for _, cs := range pod.Status.ContainerStatuses {
|
|
if waiting := cs.State.Waiting; waiting != nil && badWaitingReasons[waiting.Reason] {
|
|
return waiting.Reason
|
|
}
|
|
if term := cs.State.Terminated; term != nil && term.Reason == "OOMKilled" {
|
|
return "OOMKilled"
|
|
}
|
|
}
|
|
return ""
|
|
}
|