Files
omnex/control-plane/api/internal/demos/project_provisioner_test.go
Xor290 8d857faa45
ci-api / test (push) Successful in 23m32s
ci-web / test (push) Successful in 14m7s
chore: build
2026-09-20 18:52:09 +02:00

163 lines
5.8 KiB
Go

package demos
import (
"os"
"os/exec"
"path/filepath"
"strings"
"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"
)
func ptr[T any](v T) *T { return &v }
func TestVitrineValuesRenderWithCharts(t *testing.T) {
if _, err := exec.LookPath("helm"); err != nil {
t.Skip("helm absent du PATH")
}
chartsDir, err := filepath.Abs("../../../../deploy/chart-vitrine")
if err != nil {
t.Fatal(err)
}
h := &HelmProvisioner{cfg: &config.Config{
VitrineBackendImage: "registry.example/vitrine-backend:1.2.3",
VitrineFrontendImage: "registry.example/vitrine-frontend:1.2.3",
}}
p := ProjectSpec{
Namespace: "vitrine-ab12cd34",
Host: "vitrine-ab12cd34.vitrine-omnex.club",
URL: "https://vitrine-ab12cd34.vitrine-omnex.club",
AdminUsername: "admin",
AdminPassword: "un-mot-de-passe-solide",
AdminNumber: 3,
}
cases := []struct {
chart string
values map[string]interface{}
wants []string
}{
{"postgresql", h.buildVitrinePostgresValues("pgpass"), []string{"name: postgres\n"}},
{"redis", h.buildVitrineRedisValues("redispass"), []string{"name: redis\n"}},
{"backend", h.buildVitrineBackendValues(p, "pgpass", "redispass", "jwt"), []string{
"name: " + vitrineBackendService(p.Namespace) + "\n",
`ADMIN_NUMBER: "3"`,
`SEED_ADMIN_USERNAME: "admin"`,
"registry.example/vitrine-backend:1.2.3",
"post-install", // seedJob activé
"postgres://postgres:pgpass@postgres:5432/vitrine_db",
}},
{"frontend", h.buildVitrineFrontendValues(), []string{
"name: " + vitrineFrontendService(p.Namespace) + "\n",
"registry.example/vitrine-frontend:1.2.3",
}},
{"ingressroute", h.buildVitrineIngressRouteValues(p), []string{
"Host(`vitrine-ab12cd34.vitrine-omnex.club`)",
"name: " + vitrineBackendService(p.Namespace) + "\n",
"name: " + vitrineFrontendService(p.Namespace) + "\n",
"tls: {}", // certificat servi par le TLSStore, pas par projet
}},
}
for _, c := range cases {
t.Run(c.chart, func(t *testing.T) {
raw, err := yaml.Marshal(c.values)
if err != nil {
t.Fatal(err)
}
valuesFile := filepath.Join(t.TempDir(), "values.yaml")
if err := writeFile(valuesFile, raw); err != nil {
t.Fatal(err)
}
// Release et namespace comme installChartFrom : "<ns>-<chart>".
out, err := exec.Command("helm", "template", p.Namespace+"-"+c.chart,
filepath.Join(chartsDir, c.chart), "--namespace", p.Namespace, "--values", valuesFile).CombinedOutput()
if err != nil {
t.Fatalf("helm template: %v\n%s", err, out)
}
for _, want := range c.wants {
if !strings.Contains(string(out), want) {
t.Errorf("rendu de %s : %q introuvable", c.chart, want)
}
}
})
}
}
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)
}
}