chore: build
ci-api / test (push) Successful in 23m32s
ci-web / test (push) Successful in 14m7s

This commit is contained in:
Xor290
2026-09-20 18:52:09 +02:00
parent 9e11f01c2c
commit 8d857faa45
66 changed files with 3673 additions and 1564 deletions
@@ -1451,6 +1451,28 @@ func componentKey(podName string) string {
}
}
// podUsage : usage CPU (millicores) et mémoire (Mi) de chaque pod du namespace,
// par nom de pod. Best-effort : sans metrics-server la map est vide.
func (h *HelmProvisioner) podUsage(ctx context.Context, namespace string) map[string][2]int64 {
usage := map[string][2]int64{} // pod name -> [cpuMilli, memMi]
if h.metricsClient == nil {
return usage
}
list, err := h.metricsClient.MetricsV1beta1().PodMetricses(namespace).List(ctx, metav1.ListOptions{})
if err != nil {
return usage
}
for _, m := range list.Items {
var cpu, mem int64
for _, c := range m.Containers {
cpu += c.Usage.Cpu().MilliValue()
mem += c.Usage.Memory().Value() / (1024 * 1024)
}
usage[m.Name] = [2]int64{cpu, mem}
}
return usage
}
// GetResourceState : phase + usage CPU/mémoire live de chaque pod d'une démo.
// L'usage (metrics-server) est best-effort : s'il est indisponible, seule la
// phase du pod est renseignée plutôt que de faire échouer tout l'appel.
@@ -1467,19 +1489,7 @@ func (h *HelmProvisioner) GetResourceState(
return ResourceState{}, err
}
usage := map[string][2]int64{} // pod name -> [cpuMilli, memMi]
if h.metricsClient != nil {
if list, err := h.metricsClient.MetricsV1beta1().PodMetricses(namespace).List(ctx, metav1.ListOptions{}); err == nil {
for _, m := range list.Items {
var cpu, mem int64
for _, c := range m.Containers {
cpu += c.Usage.Cpu().MilliValue()
mem += c.Usage.Memory().Value() / (1024 * 1024)
}
usage[m.Name] = [2]int64{cpu, mem}
}
}
}
usage := h.podUsage(ctx, namespace)
var state ResourceState
@@ -1,9 +1,13 @@
package demos
import (
"context"
"fmt"
"log"
"strconv"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// Projets vitrine (voir internal/projects) : même infrastructure que les
@@ -121,6 +125,70 @@ func (h *HelmProvisioner) TeardownProject(namespace string) error {
return h.Teardown(Demo{Namespace: namespace})
}
// Valeur du label app.kubernetes.io/name des pods de chaque composant d'un
// projet vitrine (voir deploy/chart-vitrine/*/templates). Les pods des Jobs de
// migration et de création du compte admin portent d'autres noms
// (vitrine-backend-migrate / vitrine-backend-seed) : ils ne sont donc jamais
// pris pour le backend.
const (
vitrineLabelBackend = "vitrine-backend"
vitrineLabelFrontend = "vitrine-frontend"
vitrineLabelPostgreSQL = "postgresql"
vitrineLabelRedis = "redis"
)
// ProjectState : phase + usage CPU/mémoire live de chaque composant d'un projet
// vitrine, dans le même format que GetResourceState pour les démos (le panneau
// du back-office est commun). L'usage (metrics-server) est best-effort. Les
// limites viennent du pod lui-même : elles suivent les values des charts sans
// table à maintenir ici.
func (h *HelmProvisioner) ProjectState(ctx context.Context, namespace string) (ResourceState, error) {
pods, err := h.k8sClient.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{})
if err != nil {
return ResourceState{}, err
}
return buildProjectState(pods.Items, h.podUsage(ctx, namespace)), nil
}
// buildProjectState range les pods d'un projet par composant (voir
// ProjectState). usage : nom de pod -> [cpuMilli, memMi].
func buildProjectState(pods []corev1.Pod, usage map[string][2]int64) ResourceState {
var state ResourceState
for i := range pods {
pod := &pods[i]
var target *ComponentState
switch pod.Labels["app.kubernetes.io/name"] {
case vitrineLabelBackend:
target = &state.API
case vitrineLabelFrontend:
target = &state.Web
case vitrineLabelPostgreSQL:
target = &state.DB
case vitrineLabelRedis:
target = &state.DBM
default:
continue
}
cs := ComponentState{Phase: string(pod.Status.Phase)}
for _, c := range pod.Spec.Containers {
cs.CPULimitMilli += c.Resources.Limits.Cpu().MilliValue()
cs.MemoryLimitMi += c.Resources.Limits.Memory().Value() / (1024 * 1024)
}
if u, ok := usage[pod.Name]; ok {
cs.CPUMilli, cs.MemoryMi = u[0], u[1]
}
// Pendant un rolling update deux pods coexistent : celui qui tourne
// prime sur l'ancien en cours d'arrêt.
if target.Phase == string(corev1.PodRunning) && cs.Phase != string(corev1.PodRunning) {
continue
}
*target = cs
}
return state
}
func (h *HelmProvisioner) buildVitrinePostgresValues(password string) map[string]interface{} {
// Nom de base et utilisateur : défauts du chart (vitrine_db / postgres),
// alignés sur database.name / database.user du chart backend.
@@ -8,6 +8,9 @@ import (
"testing"
"go.yaml.in/yaml/v2"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/omnex/control-plane/api/internal/config"
)
@@ -88,3 +91,72 @@ func TestVitrineValuesRenderWithCharts(t *testing.T) {
}
func writeFile(path string, data []byte) error { return os.WriteFile(path, data, 0o600) }
func vitrinePod(name, component string, phase corev1.PodPhase, cpuLimit, memLimit string) corev1.Pod {
return corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: name, Labels: map[string]string{"app.kubernetes.io/name": component}},
Spec: corev1.PodSpec{Containers: []corev1.Container{{
Resources: corev1.ResourceRequirements{Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse(cpuLimit),
corev1.ResourceMemory: resource.MustParse(memLimit),
}},
}}},
Status: corev1.PodStatus{Phase: phase},
}
}
func TestBuildProjectState_MapsComponentsByLabelWithPodLimits(t *testing.T) {
pods := []corev1.Pod{
vitrinePod("postgres-0", "postgresql", corev1.PodRunning, "1", "1Gi"),
vitrinePod("redis-0", "redis", corev1.PodRunning, "500m", "512Mi"),
vitrinePod("vitrine-ab12-backend-vitrine-backend-7d5-x", "vitrine-backend", corev1.PodRunning, "500m", "256Mi"),
vitrinePod("vitrine-ab12-frontend-vitrine-frontend-9c-y", "vitrine-frontend", corev1.PodPending, "200m", "128Mi"),
}
usage := map[string][2]int64{"vitrine-ab12-backend-vitrine-backend-7d5-x": {42, 100}}
st := buildProjectState(pods, usage)
if st.DB.Phase != "Running" || st.DB.CPULimitMilli != 1000 || st.DB.MemoryLimitMi != 1024 {
t.Errorf("postgres (pod \"postgres-0\") mal reconnu : %+v", st.DB)
}
if st.DBM.Phase != "Running" || st.DBM.MemoryLimitMi != 512 {
t.Errorf("redis : %+v", st.DBM)
}
if st.API != (ComponentState{Phase: "Running", CPUMilli: 42, CPULimitMilli: 500, MemoryMi: 100, MemoryLimitMi: 256}) {
t.Errorf("backend : %+v", st.API)
}
if st.Web.Phase != "Pending" {
t.Errorf("frontend : %+v", st.Web)
}
if st.LB.Phase != "" {
t.Errorf("le load-balancer Telegram n'existe pas pour un projet : %+v", st.LB)
}
}
func TestBuildProjectState_IgnoresJobPodsAndOldReplicas(t *testing.T) {
pods := []corev1.Pod{
vitrinePod("x-backend-vitrine-backend-7d5-run", "vitrine-backend", corev1.PodRunning, "500m", "256Mi"),
// Les pods des Jobs contiennent "backend" dans leur nom mais ne sont pas le backend.
vitrinePod("x-backend-vitrine-backend-seed-abc", "vitrine-backend-seed", corev1.PodSucceeded, "500m", "256Mi"),
vitrinePod("x-backend-vitrine-backend-migrate-abc", "vitrine-backend-migrate", corev1.PodFailed, "500m", "256Mi"),
// Ancien replica en cours d'arrêt, listé après le nouveau.
vitrinePod("x-backend-vitrine-backend-old-zzz", "vitrine-backend", corev1.PodFailed, "500m", "256Mi"),
// Pod étranger au projet (sans label du chart).
{ObjectMeta: metav1.ObjectMeta{Name: "debug-shell"}},
}
st := buildProjectState(pods, nil)
if st.API.Phase != "Running" {
t.Errorf("le backend doit rester Running : %+v", st.API)
}
if st.Web.Phase != "" || st.DB.Phase != "" || st.DBM.Phase != "" {
t.Errorf("composants absents non vides : %+v", st)
}
}
func TestBuildProjectState_NoPodsIsEmpty(t *testing.T) {
if st := buildProjectState(nil, nil); st != (ResourceState{}) {
t.Errorf("état non vide : %+v", st)
}
}