Plan — Portail d'accès aux UI de la plateforme
État : Actif (2026-06-22) · Fonde : ADR 0091 (Accepted).
Promu Brouillon → Actif (et ADR 0091 Proposed → Accepted) au démarrage de
l’étape 1 — logique pure de croisement contrat ↔ état (ADR 0057).
Note (ADR 0092, 2026-06-23). L’exposition du portail a basculé du Gateway hostNetwork (L7) vers le L4 (
hostPort,http://<IP-nœud>:<port>) : le manifesteplatform/portal/gateway.yamlest retiré, remplacé par unhostPortposé sur le conteneur (modèle mailpit) et l’accès se fait par l’IP du nœud, sans DNS. Les mentions degateway.yaml/Gateway ci-dessous reflètent l’étape telle que menée à l’origine (avant ce renversement).
ADR fondateurs
Section intitulée « ADR fondateurs »- 0091 — la décision (portail dynamique, liens nouvel onglet, commandes secrets, RBAC sans secrets, expo hostNetwork).
- 0043 — le contrat
(source) ; 0048 —
access.sh(précédent à réutiliser, pas dupliquer). - 0014 / 0071 / 0023 — durcissement, exposition, valeurs génériques.
Invariants
Section intitulée « Invariants »- Source = le contrat (
contract/endpoints.example.yaml), croisé avec l’API k8s live. Aucune liste d’UI codée en dur (ADR 0023/0043). - Le pod ne lit jamais un Secret : RBAC sans verb
secrets; il affiche la commandekubectl, l’opérateur l’exécute avec ses droits. - Logique PURE testée sans cluster (croisement contrat ↔ état), comme
nestor/check_contract.py(ADR 0017) ; l’I/O (client k8s, HTTP) en bordure. - Banc d’abord : prouvé sur Lima avant la prod (ADR 0034/0053).
1. Logique pure : croisement contrat ↔ état observé
Section intitulée « 1. Logique pure : croisement contrat ↔ état observé »- CRÉER
nestor/portal.py(ouscripts/portal_view.py) :build_view(contract, observed) -> list[Entry]— pur.contract= endpoints chargés (réutiliser le loader descripts/check_contract.py) ;observed= dict injecté (services présents, endpoints prêts, hostnames Gateway/HTTPRoute, état Applications). Sortie : entrées groupées parlayer, chacune avecverdict ∈ {MATCH, MISSING, DRIFT, EXTRA},ui_url, etsecret_cmd(string de commande dérivée deauth+namespaces-secrets, JAMAIS la valeur). - CRÉER
tests/test_portal.py: verdicts (contrat∩live cohérent → MATCH ; contrat sans live → MISSING ; hostname divergent → DRIFT ; live hors contrat → EXTRA), génération dessecret_cmdpar type d’auth, groupage par layer. - Preuve SANS cluster :
pnpm test:python+ruff. Aucune brique déployée.
2. Serveur HTTP + image maison
Section intitulée « 2. Serveur HTTP + image maison »- CRÉER
platform/portal/app/: serveur HTTP Python in-cluster (clientkubernetesnatif, déjà dépendance) qui lit l’API (services, endpointslices, gateways, httproutes, applications), appellebuild_view, rend la page (sidebar par layer, lienstarget="_blank", blocssecret_cmdcopiables).Dockerfileépinglé (modèleplatform/dagster/image-openlineage/). - ÉDITER
platform-build-imagesdefaults : ajouter l’imageportal(build_all_arch: true— image maison, pas d’officielle à retaguer). - Preuve SANS cluster : build local de l’image (arm64) ; un test de rendu
(HTML contient les layers/liens/commandes attendus, sur un
observedstubé).
3. Déploiement durci : Deployment + SA + RBAC + NetworkPolicy
Section intitulée « 3. Déploiement durci : Deployment + SA + RBAC + NetworkPolicy »- CRÉER
platform/portal/portal.yaml: Namespace, ServiceAccountportal,ClusterRole+ClusterRoleBinding(get/listservices,endpointslices,gateways,httproutes,applications— aucune règlesecrets), Deployment durci (runAsNonRoot, seccomp, FS RO, no caps — ADR 0014), Service. - CRÉER
platform/network-policies/portal/allow-apiserver-egress.yaml(egress apiserver seulement, modèle dagster). - Preuve banc : déployer sur Lima ;
kubectl auth can-i get secrets --as=system:serviceaccount:portal:portal→ no (RBAC prouvé) ; pod Running, durci (kube-bench/PSA ok).
4. Exposition Gateway hostNetwork
Section intitulée « 4. Exposition Gateway hostNetwork »- CRÉER
platform/portal/gateway.yaml: Gateway + HTTPRoute Cilium hostNetwork (hostPort 443, hostnameportail.cluster.lan, TLS cert-manager) — modèleplatform/mailpit/gateway.yaml. - Preuve banc :
https://portail.cluster.lanjoignable (hostPort), liste les UI réelles du banc, verdicts cohérents (Grafana/Argo CD/Gitea/Dagster…), lessecret_cmdaffichées correspondent aux vrais Secrets, aucune valeur exposée.
5. Intégration au contrat et à l’accès
Section intitulée « 5. Intégration au contrat et à l’accès »- ÉDITER
contract/endpoints.example.yaml: ajouter l’entréeportal-ui(layersocle,ui_hostname: portail.cluster.lan,auth: none) — le portail se liste lui-même (dogfooding du contrat). - ÉDITER
bench/lima/access.sh/docs/guide-dev-data.md: pointer le portail comme vue d’ensemble (et acter le remplacement des forwards SSH par hostPort). - Preuve :
check_contract.pyreste vert (nouvelle entrée cohérente) ; docs:build OK.
6. Preuve e2e + bascule prod
Section intitulée « 6. Preuve e2e + bascule prod »- Scénario
bench/scenarios/NN-portail.sh: monte le portail au banc, sondehttps://portail.cluster.lan(HTTP 200 + présence des UI + commandes secrets), vérifie viaauth can-iqu’il ne peut pas lire un Secret. - Prod : déployé par Argo CD (GitOps) ou Ansible selon la frontière retenue ; preuve = portail dirqual joignable, reflète les 10 couches réelles. (La mutation prod reste pilotée par l’opérateur, cf. cap nestor-prod.)
- Étape 1 — logique pure + tests (
nestor/portal.py,tests/test_portal.py) - Étape 2 — serveur + image (
nestor/portal_server.py,render_html;platform/portal/image/Dockerfile) - Étape 3 — Deployment + RBAC + NetworkPolicy
(
platform/portal/portal.yaml,platform/network-policies/portal/; ClusterRole sans secrets) - Étape 4 — Gateway hostNetwork (
platform/portal/gateway.yaml,portail.cluster.lan) - Étape 5 — intégration contrat (
portal-ui) + README brique - [~] Étape 6 — scénario e2e ÉCRIT (
bench/scenarios/32-portal.sh: pod Ready + /healthz, page liste les UI,auth can-i get secrets→ no) ; PREUVE d’exécution au banc en attente (banc à monter : build image,kubectl apply,ONLY='28 32' run-all.sh), puis bascule prod.