Aller au contenu principal

Mode Kubernetes

navin-manager gère les hôtes par son agent, et les clusters Kubernetes par le Navin Platform Operator. Un cluster de type kubernetes est un cluster apporté par le client (kubeadm, RKE2, k3s, OpenShift) ou un cluster géré par un fournisseur cloud (EKS, AKS, GKE). Le manager le découvre et écrit l'état souhaité des composants du catalogue sous forme de ressources ManagedComponent (platform.navin.ai/v1alpha1) ; l'opérateur produit le chart de chaque composant, l'applique par server side apply et remonte l'état lu sur les charges réelles. La console et l'API affichent cet état confirmé, jamais l'action demandée.

Prérequis​

ÉlémentExigence
Kubernetes1.29 ou plus récent (règles de validation CEL de la CRD, stables en 1.29) ; testé avec le serveur d'API envtest 1.37.0
CRDoperator/config/crd/platform.navin.ai_managedcomponents.yaml installée par l'administrateur du cluster
Opérateuroperator/config/manager/manager.yaml, compte de service et RBAC de operator/config/rbac
Chartscharts locaux montés en lecture seule dans --charts-dir, ou charts OCI épinglés par empreinte @sha256:
Accès du managerun kubeconfig de l'utilisateur navin-manager avec les droits de operator/config/rbac/manager_access.yaml
Optionnelmetrics-server (HPA sur le CPU), KEDA (ScaledObject), une procédure de drain respectant les PDB

Le kubeconfig transmis au manager doit être autonome : certificate-authority-data, un certificat client et sa clé ou un jeton, intégrés. Le manager refuse les plugins exec (aws eks get-token, kubelogin), les auth providers, les références à des fichiers, les proxys, l'usurpation d'identité, insecure-skip-tls-verify et le http en clair.

Enregistrement et garde du kubeconfig​

POST /api/v1/clusters/{cluster_id}/kubernetes
{"api_server": "https://api.k8s.banque.example:6443", "kubeconfig": "<yaml>", "namespace": "navin-platform"}

Le serveur d'API est enregistré explicitement. Le contexte courant du kubeconfig doit pointer exactement vers cette adresse (même schéma, hôte, port et chemin), sinon la requête échoue avec 422 k8s_server_mismatch. La garde est rejouée à chaque utilisation du kubeconfig stocké : changer l'adresse enregistrée sans kubeconfig correspondant est aussi refusé. Le kubeconfig est scellé en AES-256-GCM comme tout secret et ne se relit que sous la forme {"secret": true, "set": true}. Le manager ne lit jamais KUBECONFIG, ~/.kube/config ni un compte de service du cluster. Un cluster qui gère déjà des hôtes ne peut pas devenir une cible Kubernetes (409 k8s_cluster_has_hosts).

Découverte​

POST /api/v1/clusters/{cluster_id}/kubernetes/discovery lit, par des appels en lecture seule :

  • la version du serveur d'API et la distribution (eks, gke, aks, openshift, k3s, rke2, kubernetes), qui fixe le mode : managed pour un plan de contrôle cloud, sinon byo ;
  • chaque nœud : disponibilité, rôles, taints, identifiant fournisseur, type d'instance, zone, image du système, noyau, runtime de conteneurs, version du kubelet, capacité et allocatable en CPU (millicœurs), mémoire, stockage éphémère, pods et GPU (nvidia.com/gpu, amd.com/gpu), ainsi que les totaux du cluster ;
  • les classes de stockage (provisioner, défaut, politique de récupération, mode de liaison, extension) et les classes d'ingress ;
  • CoreDNS (image et version, réplicas) ;
  • les briques de plateforme repérées par les groupes d'API que servent leurs CRD : cert-manager, Cilium, Kyverno, Argo CD, KEDA, le Navin Platform Operator, Prometheus Operator, Gateway API, instantanés de volumes, NVIDIA GPU Operator, OpenShift.

Le dernier inventaire est conservé et renvoyé par GET .../kubernetes/discovery.

Composants et opérations​

ActionRequêteEffet sur le cluster
installationPOST .../kubernetes/components {service, source, values, secret_refs, replicas, pdb, autoscaling}crée le ManagedComponent
démarrage, arrêtPOST .../components/{name}/actions {action: start ou stop}desiredState Running ou Stopped (l'arrêt met toutes les charges à zéro réplica)
redémarrage{action: restart} ou rolling_restartnouvelle annotation du modèle de pod, mise à jour progressive dans le budget de perturbation
mise à l'échelle{action: scale, replicas} ou {action: scale, autoscaling: {min_replicas, max_replicas, target_cpu_utilization_percentage, engine}}réplicas, HorizontalPodAutoscaler, ou ScaledObject KEDA quand la CRD KEDA existe
maintenance{action: maintenance, enabled}bloque l'arrêt, le redémarrage, la réduction, la configuration et la suppression
configurationPATCH .../components/{name} {version, source, values, secret_refs, pdb}nouvelles entrées du chart
suppressionDELETE .../components/{name}l'opérateur supprime tous les objets qu'il a appliqués, puis la ressource

Les valeurs ne portent jamais de secret : une clé comme password, token ou apiKey avec une valeur est refusée (422 k8s_values_secret) par le manager et par l'opérateur. Les secrets restent des Secrets Kubernetes, référencés par secret_refs (secret_name, key, value_path de la valeur du chart qui reçoit le nom du Secret).

L'état d'un composant a quatre dimensions distinctes :

ChampValeursSignification
stateinstalling, starting, started, stopping, stopped, failed, uninstallingobservé sur le statut des Deployment et StatefulSet (générations, réplicas, révisions, conditions)
healthok, warning, critical, unknownpods disponibles rapportés aux pods souhaités
operation_staterunning, succeeded, failed, blocked, idlela dernière opération demandée par la spec
security_statecompliant, non_compliant, unknownsecret en clair ou objet interdit produit par le chart

converged n'est vrai que lorsque l'opérateur a observé la dernière génération et que l'état observé égale l'état souhaité. Un arrêt bloqué par la maintenance laisse state: started et health: ok : un arrêt administratif n'est pas une suspension de sécurité et une opération bloquée n'est pas un problème de santé.

Protections​

  • PodDisruptionBudget : pdb.min_available ou pdb.max_unavailable crée un PDB par charge. La mise à jour progressive du Deployment ne descend jamais en dessous (maxUnavailable calculé depuis le budget, maxSurge 1). Un redémarrage progressif attend tant que la charge réelle a moins de pods disponibles que le budget n'en exige (operation_state: blocked, raison DisruptionBudgetExhausted) et reprend seul. Un nombre de réplicas inférieur à min_available est refusé.
  • Maintenance : les opérations perturbatrices sont refusées par le manager (409 k8s_maintenance) et par l'opérateur, quel que soit l'auteur de la ressource. L'augmentation des réplicas reste permise.
  • Les charts ne peuvent produire que des objets ConfigMap, Service, ServiceAccount, PersistentVolumeClaim, Deployment, StatefulSet, NetworkPolicy, Ingress, PodDisruptionBudget, HorizontalPodAutoscaler et ScaledObject, dans l'espace de noms du composant. Les objets RBAC, les Secrets et les objets de portée cluster sont refusés.
  • Le drain des nœuds reste une opération de l'administrateur du cluster : l'API d'éviction respecte les PDB créés par l'opérateur.

RBAC de l'opérateur (moindre privilège)​

operator/config/rbac/role.yaml est généré depuis les marqueurs RBAC du contrôleur. C'est un ClusterRole lié par un RoleBinding par espace de noms géré (role_binding.yaml), jamais par un ClusterRoleBinding, et l'opérateur ne surveille que les espaces de noms donnés à --namespaces.

RessourcesVerbes
managedcomponents, status, finalizersget, list, watch, update, patch (status et finalizers : update)
deployments, statefulsets, configmaps, services, serviceaccounts, persistentvolumeclaimsget, list, watch, create, update, patch, delete
networkpolicies, ingresses, poddisruptionbudgets, horizontalpodautoscalers, scaledobjectsget, list, watch, create, update, patch, delete
secretsget (existence d'un Secret référencé et de sa clé, jamais list ni watch)
leasesget, create, update (élection du leader)

L'opérateur tourne sans root, avec un système de fichiers racine en lecture seule, sans élévation de privilèges et sans aucune capability. Hors cluster, il n'accepte qu'un --kubeconfig explicite accompagné de --api-server, avec la même garde que le manager.

Ce que le manager fait et ne fait jamais sur un cluster du client​

Il fait : lire la version, les nœuds, les classes de stockage, les classes d'ingress, le Deployment CoreDNS et les groupes d'API ; créer, modifier et supprimer des ressources ManagedComponent dans l'espace de noms enregistré ; lire leur statut.

Il ne fait jamais : créer ou supprimer des espaces de noms, des nœuds, des CRD, des objets RBAC ou des Secrets ; lire la valeur d'un Secret ; cordon, drain ou éviction ; installer l'opérateur ou la CRD (l'administrateur du cluster s'en charge, par GitOps) ; exécuter une commande sur un nœud ; utiliser un autre identifiant que le kubeconfig enregistré. Désenregistrer la cible ou supprimer le cluster du manager laisse les charges en marche : supprimez d'abord les composants.

Tests​

Les tests de l'opérateur (cd navin-manager/operator && go test ./...) et ceux du serveur (tests/test_kubernetes_*.py) démarrent un kube-apiserver et un etcd avec controller-runtime envtest, épinglés sur Kubernetes 1.37.0 dans /tmp/envtest, jamais un cluster existant. envtest n'a ni kubelet ni controller manager : les tests écrivent le statut des Deployment et StatefulSet qu'écriraient ces contrôleurs et vérifient que l'état observé converge. Une chaîne Go ou un binaire envtest absent fait échouer les tests.

/tmp/gopath/bin/setup-envtest use 1.37.0 --bin-dir /tmp/envtest
cd navin-manager/operator && go vet ./... && go test ./...
cd navin-manager/server && .venv/bin/python -m pytest -q tests/test_kubernetes_unit.py tests/test_kubernetes_api.py tests/test_kubernetes_operator.py

Runtime MCP (ToolHive Kubernetes Operator)​

Le manager installe le ToolHive Kubernetes Operator (Stacklok, Apache-2.0) et suit les instances MCP éphémères que le contrôleur de cycle de vie du Control Plane crée par son intermédiaire (page « Runtime MCP » de la section Cluster ; bouton « Installer ToolHive » aussi dans la page Kubernetes).

RoutePermissionRôle
GET /api/v1/clusters/{cluster_id}/mcp-runtimecluster:readDeployment toolhive-operator de toolhive-system (prêt, réplicas, version), CRD MCPServer servie ou non, ManagedComponents de ToolHive, instances gérées de tous les espaces de noms (tenant, projet, modèle, empreinte du propriétaire, phase, adresse, âge, espace de noms) et décomptes par phase, tenant et projet ; tenant et project filtrent les éléments
POST /api/v1/clusters/{cluster_id}/mcp-runtime/installservices:installécrit les ManagedComponents toolhive-operator-crds puis toolhive-operator dans toolhive-system (409 mcp_runtime_installed si l'un existe)
DELETE /api/v1/clusters/{cluster_id}/mcp-runtime/instances/{namespace}/{name}services:operatesupprime un MCPServer géré avec une précondition sur son uid
POST /api/v1/clusters/{cluster_id}/mcp-runtime/instances/{namespace}/{name}/restartservices:operateredémarre un MCPServer géré : patch de fusion de l'annotation mcpserver.toolhive.stacklok.dev/restarted-at avec un nouvel horodatage RFC 3339 (redémarrage progressif, précondition sur l'uid, audité)

Charts épinglés (version 0.59.0, empreintes des manifestes OCI lues sur ghcr.io) :

  • oci://ghcr.io/stacklok/toolhive/toolhive-operator-crds@sha256:b7694eb741a04043818978c34b0136cf8cef30329a95ae16de518bd02daaf620
  • oci://ghcr.io/stacklok/toolhive/toolhive-operator@sha256:0920e45a4d371a6a9096c0019afea688e3aecbd1167182f5dacfe86b37605449

Valeurs de l'opérateur : 2 réplicas (l'élection de leader garde un seul contrôleur actif), operator.rbac.scope: cluster, aucune fonction expérimentale, pas d'autoscaling, un budget de perturbation d'un pod et le contexte de sécurité restreint du chart écrit explicitement (non root, système de fichiers racine en lecture seule, pas d'élévation de privilèges, toutes les capacités retirées, seccomp RuntimeDefault).

Seuls les objets MCPServer étiquetés app.kubernetes.io/managed-by=eai-mcp-runtime dans un espace de noms mcp-<12 hex> étiqueté eai.io/mcp-runtime=true sont listés ou supprimés. Tout autre objet répond 404 mcp_runtime_instance_not_found, comme un objet absent, et le refus est audité (mcp_runtime.instance.destroy, résultat denied, avec la raison). Le propriétaire n'apparaît que sous l'empreinte de 16 hex de eai.io/owner, jamais sous une identité. Supprimer un MCPServer géré est sûr : le contrôleur du Control Plane révoque ensuite les identifiants temporaires et clôt l'instance.

Droits nécessaires à l'identité Kubernetes du manager (listés dans preset.required_rights ; un droit manquant répond 502 mcp_runtime_forbidden avec le verbe et la ressource) : get du Deployment toolhive-operator de toolhive-system, get et list des espaces de noms, get, list et delete de mcpservers.toolhive.stacklok.dev, et les ManagedComponents de toolhive-system. L'identité d'import (navin-import) ne les accorde pas par défaut.

Limite : le Navin Platform Operator ne rend que les types namespacés de sa liste d'autorisation (operator/internal/controller/workloads.go). Les charts ToolHive contiennent des CustomResourceDefinitions et des objets RBAC du cluster, hors de cette liste : tant que l'opérateur ne les accepte pas, ToolHive lui-même est installé par le déploiement de la plateforme et le manager le suit.

cd navin-manager/server && .venv/bin/python -m pytest -q tests/test_mcp_runtime_k8s.py
cd navin-manager/ui && npx vitest run src/test/mcp-runtime.test.tsx