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ément | Exigence |
|---|---|
| Kubernetes | 1.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 |
| CRD | operator/config/crd/platform.navin.ai_managedcomponents.yaml installée par l'administrateur du cluster |
| Opérateur | operator/config/manager/manager.yaml, compte de service et RBAC de operator/config/rbac |
| Charts | charts locaux montés en lecture seule dans --charts-dir, ou charts OCI épinglés par empreinte @sha256: |
| Accès du manager | un kubeconfig de l'utilisateur navin-manager avec les droits de operator/config/rbac/manager_access.yaml |
| Optionnel | metrics-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 :managedpour un plan de contrôle cloud, sinonbyo; - 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
| Action | Requête | Effet sur le cluster |
|---|---|---|
| installation | POST .../kubernetes/components {service, source, values, secret_refs, replicas, pdb, autoscaling} | crée le ManagedComponent |
| démarrage, arrêt | POST .../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_restart | nouvelle 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 |
| configuration | PATCH .../components/{name} {version, source, values, secret_refs, pdb} | nouvelles entrées du chart |
| suppression | DELETE .../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 :
| Champ | Valeurs | Signification |
|---|---|---|
state | installing, starting, started, stopping, stopped, failed, uninstalling | observé sur le statut des Deployment et StatefulSet (générations, réplicas, révisions, conditions) |
health | ok, warning, critical, unknown | pods disponibles rapportés aux pods souhaités |
operation_state | running, succeeded, failed, blocked, idle | la dernière opération demandée par la spec |
security_state | compliant, non_compliant, unknown | secret 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_availableoupdb.max_unavailablecrée un PDB par charge. La mise à jour progressive du Deployment ne descend jamais en dessous (maxUnavailablecalculé depuis le budget,maxSurge1). 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, raisonDisruptionBudgetExhausted) et reprend seul. Un nombre de réplicas inférieur àmin_availableest 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.
| Ressources | Verbes |
|---|---|
| managedcomponents, status, finalizers | get, list, watch, update, patch (status et finalizers : update) |
| deployments, statefulsets, configmaps, services, serviceaccounts, persistentvolumeclaims | get, list, watch, create, update, patch, delete |
| networkpolicies, ingresses, poddisruptionbudgets, horizontalpodautoscalers, scaledobjects | get, list, watch, create, update, patch, delete |
| secrets | get (existence d'un Secret référencé et de sa clé, jamais list ni watch) |
| leases | get, 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).
| Route | Permission | Rôle |
|---|---|---|
GET /api/v1/clusters/{cluster_id}/mcp-runtime | cluster:read | Deployment 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/install | services: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:operate | supprime un MCPServer géré avec une précondition sur son uid |
POST /api/v1/clusters/{cluster_id}/mcp-runtime/instances/{namespace}/{name}/restart | services:operate | redé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:b7694eb741a04043818978c34b0136cf8cef30329a95ae16de518bd02daaf620oci://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