@@ -19,6 +19,7 @@ import (
|
||||
"go.yaml.in/yaml/v2"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
k8sCoreV1 "k8s.io/api/core/v1"
|
||||
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/types"
|
||||
@@ -87,8 +88,9 @@ func NewHelmProvisioner(
|
||||
// EnsureSharedInfra), jamais par démo — chaque démo installe seulement le
|
||||
// chart "ingressroute" qui la raccorde à ce Traefik partagé.
|
||||
const (
|
||||
sharedTraefikNamespace = "traefik"
|
||||
sharedTraefikRelease = "gestion-traefik"
|
||||
sharedTraefikNamespace = "traefik"
|
||||
sharedTraefikRelease = "gestion-traefik"
|
||||
sharedRegistryCredentialsRelease = "gestion-registry-credentials"
|
||||
)
|
||||
|
||||
// Provision déploie une démo avec Helm :
|
||||
@@ -113,6 +115,24 @@ func (h *HelmProvisioner) Provision(d Demo, resources []ExternalResource, cfg Pr
|
||||
return fmt.Errorf("déploiement network-policy: %w", err)
|
||||
}
|
||||
|
||||
// Plafond agrégé de ressources (voir deploy/chart-gestion/resource-quota) :
|
||||
// sans ça, l'autoscaling (HPA) du backend/frontend d'une démo peut monter
|
||||
// en charge sans limite globale et affamer les autres démos du même
|
||||
// cluster — les limites par pod ne bornent que l'individuel, pas l'agrégat.
|
||||
if err := h.installChart(d.Namespace, "resource-quota", nil); err != nil {
|
||||
h.deleteNamespace(d.Namespace)
|
||||
return fmt.Errorf("déploiement resource-quota: %w", err)
|
||||
}
|
||||
|
||||
// Identifiants Docker Hub (voir deploy/chart-gestion/registry-credentials) :
|
||||
// AVANT tout chart qui pull une image, sinon les pods restent en
|
||||
// ImagePullBackOff si le rate-limit anonyme est déjà atteint sur ce nœud.
|
||||
// No-op si aucun compte n'est configuré (imagePullSecretsValue == nil).
|
||||
if err := h.installChart(d.Namespace, "registry-credentials", h.buildRegistryCredentialsValues()); err != nil {
|
||||
h.deleteNamespace(d.Namespace)
|
||||
return fmt.Errorf("déploiement registry-credentials: %w", err)
|
||||
}
|
||||
|
||||
// Mot de passe postgres/redis généré par démo (jamais réutilisé d'une
|
||||
// démo à l'autre) : avec des identifiants partagés, un gap réseau
|
||||
// (CNI, règle manquante) donnerait un accès direct aux données de
|
||||
@@ -207,11 +227,29 @@ func (h *HelmProvisioner) EnsureSharedInfra() error {
|
||||
return fmt.Errorf("création namespace %s: %w", sharedTraefikNamespace, err)
|
||||
}
|
||||
|
||||
// Identifiants Docker Hub (voir deploy/chart-gestion/registry-credentials) :
|
||||
// AVANT traefik, qui pull son image + le plugin WASM Coraza. No-op si
|
||||
// aucun compte n'est configuré.
|
||||
if err := h.upgradeInstallChart(sharedTraefikNamespace, sharedRegistryCredentialsRelease, "registry-credentials", h.buildRegistryCredentialsValues()); err != nil {
|
||||
return fmt.Errorf("déploiement registry-credentials partagé: %w", err)
|
||||
}
|
||||
|
||||
// Le subchart officiel traefik/traefik est vendorisé dans le repo
|
||||
// (deploy/chart-gestion/traefik/charts/traefik-*.tgz + Chart.lock) :
|
||||
// pas de "helm dependency update" au runtime, qui échouerait de toute
|
||||
// façon puisque /charts est monté en lecture seule.
|
||||
if err := h.upgradeInstallChart(sharedTraefikNamespace, sharedTraefikRelease, "traefik", nil); err != nil {
|
||||
// "deployment.imagePullSecrets", pas "imagePullSecrets" à la racine :
|
||||
// emplacement imposé par values.schema.json du chart officiel
|
||||
// traefik/traefik (voir le commentaire équivalent dans
|
||||
// deploy/chart-gestion/traefik/values.yaml).
|
||||
traefikValues := map[string]interface{}{
|
||||
"traefik": map[string]interface{}{
|
||||
"deployment": map[string]interface{}{
|
||||
"imagePullSecrets": h.imagePullSecretsValue(),
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := h.upgradeInstallChart(sharedTraefikNamespace, sharedTraefikRelease, "traefik", traefikValues); err != nil {
|
||||
return fmt.Errorf("déploiement traefik partagé: %w", err)
|
||||
}
|
||||
|
||||
@@ -278,6 +316,14 @@ func (h *HelmProvisioner) MigrateToPremiumNamespace(d Demo, newNamespace, newURL
|
||||
h.deleteNamespace(newNamespace)
|
||||
return fmt.Errorf("déploiement network-policy: %w", err)
|
||||
}
|
||||
if err := h.installChart(newNamespace, "resource-quota", nil); err != nil {
|
||||
h.deleteNamespace(newNamespace)
|
||||
return fmt.Errorf("déploiement resource-quota: %w", err)
|
||||
}
|
||||
if err := h.installChart(newNamespace, "registry-credentials", h.buildRegistryCredentialsValues()); err != nil {
|
||||
h.deleteNamespace(newNamespace)
|
||||
return fmt.Errorf("déploiement registry-credentials: %w", err)
|
||||
}
|
||||
|
||||
newDemo := d
|
||||
newDemo.Namespace = newNamespace
|
||||
@@ -648,7 +694,23 @@ func (h *HelmProvisioner) execInPod(ctx context.Context, namespace, pod, contain
|
||||
return stdout.String(), stderrBuf.String(), nil
|
||||
}
|
||||
|
||||
// createNamespace crée un namespace Kubernetes.
|
||||
// omnexServiceAccountNamespace / omnexServiceAccountName / omnexWorkloadClusterRole :
|
||||
// identité du ServiceAccount du control-plane (voir deploy/rbac/control-plane.yml).
|
||||
// omnexWorkloadClusterRole n'est JAMAIS liée cluster-wide — seulement via un
|
||||
// RoleBinding par namespace (voir ensureWorkloadRoleBinding), pour que le
|
||||
// ServiceAccount n'ait aucun accès aux namespaces qu'il ne gère pas
|
||||
// (cert-manager, longhorn-system, kube-system...). Le ServiceAccount ne
|
||||
// possède que le verbe "bind" sur cette ClusterRole précise : il ne peut pas
|
||||
// s'octroyer davantage (mécanisme RBAC anti-escalade de Kubernetes).
|
||||
const (
|
||||
omnexServiceAccountNamespace = "omnex-system"
|
||||
omnexServiceAccountName = "omnex-control-plane"
|
||||
omnexWorkloadClusterRole = "omnex-demo-workload-manager"
|
||||
)
|
||||
|
||||
// createNamespace crée un namespace Kubernetes et y lie la ClusterRole
|
||||
// métier du control-plane (moindre privilège — voir la doc des constantes
|
||||
// ci-dessus).
|
||||
func (h *HelmProvisioner) createNamespace(name string) error {
|
||||
ns := &k8sCoreV1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
@@ -662,6 +724,34 @@ func (h *HelmProvisioner) createNamespace(name string) error {
|
||||
if err != nil && !k8sErrors.IsAlreadyExists(err) {
|
||||
return err
|
||||
}
|
||||
return h.ensureWorkloadRoleBinding(name)
|
||||
}
|
||||
|
||||
// ensureWorkloadRoleBinding lie omnexWorkloadClusterRole au ServiceAccount du
|
||||
// control-plane, dans "namespace" uniquement — c'est ce RoleBinding (pas une
|
||||
// ClusterRoleBinding) qui borne les droits du control-plane à ce seul
|
||||
// namespace pour les Deployments/Secrets/ConfigMaps/etc.
|
||||
func (h *HelmProvisioner) ensureWorkloadRoleBinding(namespace string) error {
|
||||
rb := &rbacv1.RoleBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: omnexServiceAccountName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
RoleRef: rbacv1.RoleRef{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "ClusterRole",
|
||||
Name: omnexWorkloadClusterRole,
|
||||
},
|
||||
Subjects: []rbacv1.Subject{{
|
||||
Kind: "ServiceAccount",
|
||||
Name: omnexServiceAccountName,
|
||||
Namespace: omnexServiceAccountNamespace,
|
||||
}},
|
||||
}
|
||||
_, err := h.k8sClient.RbacV1().RoleBindings(namespace).Create(context.Background(), rb, metav1.CreateOptions{})
|
||||
if err != nil && !k8sErrors.IsAlreadyExists(err) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -693,6 +783,13 @@ func (h *HelmProvisioner) installChart(namespace, chartName string, values map[s
|
||||
"--wait",
|
||||
"--atomic",
|
||||
"--timeout", "5m",
|
||||
// Le ServiceAccount du control-plane n'a volontairement aucun droit
|
||||
// cluster-scope sur les CustomResourceDefinition (voir
|
||||
// deploy/rbac/control-plane.yml) : les CRD sont appliquées une fois,
|
||||
// à part, par un opérateur humain avec un accès admin — jamais par
|
||||
// ce chemin. Sans ce flag, "helm install" tente quand même de les
|
||||
// créer (même si elles existent déjà) et échoue en Forbidden.
|
||||
"--skip-crds",
|
||||
}
|
||||
if err := h.runHelm(args...); err != nil {
|
||||
return fmt.Errorf("helm install %s: %w", chartName, err)
|
||||
@@ -714,6 +811,12 @@ func (h *HelmProvisioner) upgradeInstallChart(namespace, release, chartName stri
|
||||
"--wait",
|
||||
"--atomic",
|
||||
"--timeout", "5m",
|
||||
// Voir le commentaire équivalent dans installChart : le
|
||||
// ServiceAccount du control-plane n'a pas le droit de créer des CRD
|
||||
// (deploy/rbac/control-plane.yml) — utilisé ici pour le chart
|
||||
// "traefik", dont le subchart vendorisé embarque des CRD déjà
|
||||
// appliquées une fois à part par un opérateur admin.
|
||||
"--skip-crds",
|
||||
}
|
||||
if values != nil {
|
||||
valuesFile, cleanup, err := h.writeValuesFile(namespace, chartName, values)
|
||||
@@ -765,6 +868,38 @@ func (h *HelmProvisioner) runHelmOutput(args ...string) ([]byte, error) {
|
||||
return stdout.Bytes(), nil
|
||||
}
|
||||
|
||||
// dockerHubSecretName : nom fixe du Secret créé par le chart
|
||||
// registry-credentials (voir buildRegistryCredentialsValues) — référencé
|
||||
// tel quel par imagePullSecretsValue, toujours dans le même namespace.
|
||||
const dockerHubSecretName = "dockerhub-pull-secret"
|
||||
|
||||
// imagePullSecretsValue : nil si aucun compte Docker Hub n'est configuré
|
||||
// (OMNEX_DOCKERHUB_USERNAME/PASSWORD vides — pull anonyme, comportement par
|
||||
// défaut inchangé). Un map Helm avec une valeur nil pour une clé existante
|
||||
// du chart fait retomber sur le défaut du chart ("[]"), donc sans danger à
|
||||
// assigner inconditionnellement.
|
||||
func (h *HelmProvisioner) imagePullSecretsValue() []map[string]interface{} {
|
||||
if h.cfg.DockerHubUsername == "" || h.cfg.DockerHubPassword == "" {
|
||||
return nil
|
||||
}
|
||||
return []map[string]interface{}{{"name": dockerHubSecretName}}
|
||||
}
|
||||
|
||||
// buildRegistryCredentialsValues construit les valeurs pour le chart
|
||||
// registry-credentials (voir Provision/EnsureSharedInfra/
|
||||
// MigrateToPremiumNamespace) — nil si pas de compte configuré, auquel cas
|
||||
// installChart écrit un fichier de valeurs "null" (no-op, voir writeValuesFile).
|
||||
func (h *HelmProvisioner) buildRegistryCredentialsValues() map[string]interface{} {
|
||||
if h.cfg.DockerHubUsername == "" || h.cfg.DockerHubPassword == "" {
|
||||
return nil
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"secretName": dockerHubSecretName,
|
||||
"username": h.cfg.DockerHubUsername,
|
||||
"password": h.cfg.DockerHubPassword,
|
||||
}
|
||||
}
|
||||
|
||||
func parseImage(image string) (repo string, tag string) {
|
||||
if image == "" {
|
||||
return "", "helm"
|
||||
@@ -860,7 +995,8 @@ func (h *HelmProvisioner) buildBackendValues(d Demo, resources []ExternalResourc
|
||||
}
|
||||
|
||||
values := map[string]interface{}{
|
||||
"replicaCount": 1,
|
||||
"replicaCount": 1,
|
||||
"imagePullSecrets": h.imagePullSecretsValue(),
|
||||
"image": map[string]interface{}{
|
||||
"repository": backendRepo,
|
||||
"tag": backendTag,
|
||||
@@ -894,7 +1030,8 @@ func (h *HelmProvisioner) buildFrontendValues(d Demo) map[string]interface{} {
|
||||
frontendRepo, frontendTag := parseImage(h.frontendImage)
|
||||
|
||||
return map[string]interface{}{
|
||||
"replicaCount": 1,
|
||||
"replicaCount": 1,
|
||||
"imagePullSecrets": h.imagePullSecretsValue(),
|
||||
"image": map[string]interface{}{
|
||||
"repository": frontendRepo,
|
||||
"tag": frontendTag,
|
||||
@@ -977,6 +1114,7 @@ func (h *HelmProvisioner) buildLBTelegramValues(d Demo, cfg ProvisionConfig, bac
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"imagePullSecrets": h.imagePullSecretsValue(),
|
||||
"image": map[string]interface{}{
|
||||
"repository": lbRepo,
|
||||
"tag": lbTag,
|
||||
@@ -1028,6 +1166,7 @@ func (h *HelmProvisioner) buildSecrets(resources []ExternalResource) map[string]
|
||||
// jamais partagé (voir Provision/MigrateToPremiumNamespace).
|
||||
func (h *HelmProvisioner) buildPostgresValues(d Demo, password string) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"imagePullSecrets": h.imagePullSecretsValue(),
|
||||
"auth": map[string]interface{}{
|
||||
"password": password,
|
||||
"username": "postgres",
|
||||
@@ -1048,6 +1187,7 @@ func (h *HelmProvisioner) buildPostgresValues(d Demo, password string) map[strin
|
||||
// jamais partagé (voir Provision/MigrateToPremiumNamespace).
|
||||
func (h *HelmProvisioner) buildRedisValues(d Demo, password string) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"imagePullSecrets": h.imagePullSecretsValue(),
|
||||
"service": map[string]interface{}{
|
||||
"type": "ClusterIP",
|
||||
"port": 6379,
|
||||
|
||||
Reference in New Issue
Block a user