|
|
|
@@ -22,7 +22,10 @@ import (
|
|
|
|
|
rbacv1 "k8s.io/api/rbac/v1"
|
|
|
|
|
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
|
|
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
|
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
|
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
|
|
|
"k8s.io/apimachinery/pkg/types"
|
|
|
|
|
"k8s.io/client-go/dynamic"
|
|
|
|
|
"k8s.io/client-go/kubernetes"
|
|
|
|
|
"k8s.io/client-go/kubernetes/scheme"
|
|
|
|
|
"k8s.io/client-go/rest"
|
|
|
|
@@ -37,6 +40,7 @@ type HelmProvisioner struct {
|
|
|
|
|
k8sClient *kubernetes.Clientset
|
|
|
|
|
restConfig *rest.Config // pour l'exec dans les pods (pg_dump/restore)
|
|
|
|
|
metricsClient *metricsclient.Clientset // optionnel : nil si metrics-server indisponible
|
|
|
|
|
dynamicClient dynamic.Interface // pour les CRD Velero (Schedule/Backup), voir ensureDemoBackupSchedule
|
|
|
|
|
chartsDir string // chemin vers le dossier des charts (ex: /charts)
|
|
|
|
|
frontendImage string
|
|
|
|
|
backendImage string
|
|
|
|
@@ -69,11 +73,17 @@ func NewHelmProvisioner(
|
|
|
|
|
return nil, fmt.Errorf("helm exécutable non trouvé à %s: %w", helmPath, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dynamicClient, err := dynamic.NewForConfig(restConfig)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("client dynamique (CRD Velero): %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &HelmProvisioner{
|
|
|
|
|
cfg: cfg,
|
|
|
|
|
k8sClient: k8sClient,
|
|
|
|
|
restConfig: restConfig,
|
|
|
|
|
metricsClient: metricsClient,
|
|
|
|
|
dynamicClient: dynamicClient,
|
|
|
|
|
chartsDir: chartsDir,
|
|
|
|
|
frontendImage: cfg.FrontendImage,
|
|
|
|
|
backendImage: cfg.BackendImage,
|
|
|
|
@@ -106,6 +116,12 @@ func (h *HelmProvisioner) Provision(d Demo, resources []ExternalResource, cfg Pr
|
|
|
|
|
return fmt.Errorf("création namespace %s: %w", d.Namespace, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sauvegardes (voir ensureDemoBackupSchedule) : non-fatal, une démo doit
|
|
|
|
|
// pouvoir fonctionner même si Velero est momentanément indisponible.
|
|
|
|
|
if err := h.ensureDemoBackupSchedule(d.Namespace); err != nil {
|
|
|
|
|
log.Printf("Warning: planning de sauvegarde échoué pour %s: %v", d.Namespace, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Isolation réseau AVANT tout workload : deny-by-default, aucune
|
|
|
|
|
// communication inter-démos, ingress limité au Traefik partagé (voir
|
|
|
|
|
// deploy/chart-gestion/network-policy). Sans ça, tout pod d'une démo
|
|
|
|
@@ -258,8 +274,18 @@ func (h *HelmProvisioner) EnsureSharedInfra() error {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Teardown détruit une démo en supprimant son namespace (cascading) — les
|
|
|
|
|
// PVC postgres/redis de la démo partent avec, puisqu'ils sont namespaced.
|
|
|
|
|
// PVC postgres/redis de la démo partent avec, puisqu'ils sont namespaced —
|
|
|
|
|
// et ses sauvegardes Velero (planning + Backup déjà pris, voir
|
|
|
|
|
// deleteDemoBackups). Appelée aussi bien pour l'expiration TTL que pour une
|
|
|
|
|
// destruction forcée par un admin (même chemin, voir Service.Delete) : dans
|
|
|
|
|
// les deux cas, les sauvegardes associées disparaissent avec la démo.
|
|
|
|
|
// La suppression des sauvegardes échoue en silence (journalisée) : un
|
|
|
|
|
// pépin côté Velero ne doit pas bloquer la destruction de la démo
|
|
|
|
|
// elle-même, qui reste l'action prioritaire demandée par l'appelant.
|
|
|
|
|
func (h *HelmProvisioner) Teardown(d Demo) error {
|
|
|
|
|
if err := h.deleteDemoBackups(d.Namespace); err != nil {
|
|
|
|
|
log.Printf("Warning: suppression des sauvegardes échouée pour %s: %v", d.Namespace, err)
|
|
|
|
|
}
|
|
|
|
|
return h.deleteNamespace(d.Namespace)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -311,6 +337,11 @@ func (h *HelmProvisioner) MigrateToPremiumNamespace(d Demo, newNamespace, newURL
|
|
|
|
|
return fmt.Errorf("création namespace %s: %w", newNamespace, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sauvegardes : voir le commentaire équivalent dans Provision. Non-fatal.
|
|
|
|
|
if err := h.ensureDemoBackupSchedule(newNamespace); err != nil {
|
|
|
|
|
log.Printf("Warning: planning de sauvegarde échoué pour %s: %v", newNamespace, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Isolation réseau AVANT tout workload, comme pour Provision.
|
|
|
|
|
if err := h.installChart(newNamespace, "network-policy", h.buildNetworkPolicyValues()); err != nil {
|
|
|
|
|
h.deleteNamespace(newNamespace)
|
|
|
|
@@ -389,6 +420,13 @@ func (h *HelmProvisioner) MigrateToPremiumNamespace(d Demo, newNamespace, newURL
|
|
|
|
|
return fmt.Errorf("rollout du nouveau namespace %s: %w", newNamespace, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sauvegardes de l'ancien namespace : plus de sens une fois migré (les
|
|
|
|
|
// données sont déjà dans le nouveau namespace, qui a son propre
|
|
|
|
|
// planning depuis sa création ci-dessus).
|
|
|
|
|
if err := h.deleteDemoBackups(oldNamespace); err != nil {
|
|
|
|
|
log.Printf("Warning: suppression des sauvegardes de l'ancien namespace %s échouée: %v", oldNamespace, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := h.deleteNamespace(oldNamespace); err != nil {
|
|
|
|
|
log.Printf("Warning: suppression ancien namespace %s échouée (nouveau namespace %s opérationnel): %v", oldNamespace, newNamespace, err)
|
|
|
|
|
}
|
|
|
|
@@ -763,6 +801,94 @@ func (h *HelmProvisioner) deleteNamespace(name string) error {
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// veleroNamespace : namespace fixe où tourne Velero (installé une fois par
|
|
|
|
|
// un opérateur admin, voir deploy/velero/) — jamais créé par ce code.
|
|
|
|
|
//
|
|
|
|
|
// Sauvegardes des démos : un Schedule Velero PAR démo (pas un planning
|
|
|
|
|
// global multi-namespace) — c'est le seul moyen de supprimer les
|
|
|
|
|
// sauvegardes d'UNE démo sans toucher à celles des autres, Velero ne
|
|
|
|
|
// permettant de supprimer qu'un objet Backup entier, jamais une portion
|
|
|
|
|
// (voir ensureDemoBackupSchedule/deleteDemoBackups, appelés depuis
|
|
|
|
|
// Provision/Teardown/MigrateToPremiumNamespace).
|
|
|
|
|
const veleroNamespace = "velero"
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
veleroScheduleGVR = schema.GroupVersionResource{Group: "velero.io", Version: "v1", Resource: "schedules"}
|
|
|
|
|
veleroBackupGVR = schema.GroupVersionResource{Group: "velero.io", Version: "v1", Resource: "backups"}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// demoBackupScheduleName : nom du Schedule Velero dédié à ce namespace de
|
|
|
|
|
// démo (aussi utilisé pour retrouver ses Backup via le label
|
|
|
|
|
// velero.io/schedule-name que Velero pose automatiquement).
|
|
|
|
|
func demoBackupScheduleName(namespace string) string {
|
|
|
|
|
return "demo-backup-" + namespace
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ensureDemoBackupSchedule crée (idempotent) le Schedule Velero dédié à ce
|
|
|
|
|
// namespace de démo : sauvegarde complète (manifestes + contenu réel des
|
|
|
|
|
// PVC via File System Backup) toutes les 15 min, conservée 24h — fenêtre de
|
|
|
|
|
// perte de données minimale en cas de problème sur le cluster. Non-fatal en
|
|
|
|
|
// cas d'erreur côté appelant (Velero indisponible ne doit pas empêcher une
|
|
|
|
|
// démo de fonctionner) : voir les appels dans Provision/
|
|
|
|
|
// MigrateToPremiumNamespace, qui journalisent seulement.
|
|
|
|
|
func (h *HelmProvisioner) ensureDemoBackupSchedule(namespace string) error {
|
|
|
|
|
obj := &unstructured.Unstructured{Object: map[string]interface{}{
|
|
|
|
|
"apiVersion": "velero.io/v1",
|
|
|
|
|
"kind": "Schedule",
|
|
|
|
|
"metadata": map[string]interface{}{
|
|
|
|
|
"name": demoBackupScheduleName(namespace),
|
|
|
|
|
"namespace": veleroNamespace,
|
|
|
|
|
},
|
|
|
|
|
"spec": map[string]interface{}{
|
|
|
|
|
"schedule": "*/15 * * * *",
|
|
|
|
|
"template": map[string]interface{}{
|
|
|
|
|
"includedNamespaces": []interface{}{namespace},
|
|
|
|
|
"defaultVolumesToFsBackup": true,
|
|
|
|
|
"storageLocation": "default",
|
|
|
|
|
"ttl": "24h0m0s",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}}
|
|
|
|
|
_, err := h.dynamicClient.Resource(veleroScheduleGVR).Namespace(veleroNamespace).
|
|
|
|
|
Create(context.Background(), obj, metav1.CreateOptions{})
|
|
|
|
|
if err != nil && !k8sErrors.IsAlreadyExists(err) {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// deleteDemoBackups supprime le Schedule Velero de cette démo ainsi que
|
|
|
|
|
// toutes les sauvegardes déjà prises par ce planning (la suppression d'un
|
|
|
|
|
// Backup déclenche le nettoyage asynchrone des données Kopia associées côté
|
|
|
|
|
// Velero — pas de suppression directe dans le bucket S3, qui risquerait de
|
|
|
|
|
// casser le dépôt Kopia partagé par les autres démos). Appelée depuis
|
|
|
|
|
// Teardown (expiration TTL et destruction forcée passent toutes les deux
|
|
|
|
|
// par ce même chemin) et lors d'une migration premium réussie (l'ancien
|
|
|
|
|
// namespace disparaît, son planning de sauvegarde n'a plus de sens).
|
|
|
|
|
func (h *HelmProvisioner) deleteDemoBackups(namespace string) error {
|
|
|
|
|
name := demoBackupScheduleName(namespace)
|
|
|
|
|
|
|
|
|
|
list, err := h.dynamicClient.Resource(veleroBackupGVR).Namespace(veleroNamespace).
|
|
|
|
|
List(context.Background(), metav1.ListOptions{LabelSelector: "velero.io/schedule-name=" + name})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("liste des sauvegardes de %s: %w", namespace, err)
|
|
|
|
|
}
|
|
|
|
|
for _, b := range list.Items {
|
|
|
|
|
err := h.dynamicClient.Resource(veleroBackupGVR).Namespace(veleroNamespace).
|
|
|
|
|
Delete(context.Background(), b.GetName(), metav1.DeleteOptions{})
|
|
|
|
|
if err != nil && !k8sErrors.IsNotFound(err) {
|
|
|
|
|
return fmt.Errorf("suppression sauvegarde %s: %w", b.GetName(), err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err = h.dynamicClient.Resource(veleroScheduleGVR).Namespace(veleroNamespace).
|
|
|
|
|
Delete(context.Background(), name, metav1.DeleteOptions{})
|
|
|
|
|
if err != nil && !k8sErrors.IsNotFound(err) {
|
|
|
|
|
return fmt.Errorf("suppression planning %s: %w", name, err)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// installChart installe (première install, jamais réappliqué) un chart Helm
|
|
|
|
|
// avec des valeurs personnalisées. Release name : "<namespace>-<chartName>".
|
|
|
|
|
// Adapté aux charts par démo (namespace toujours neuf).
|
|
|
|
|