package fission import ( "context" "fmt" "log" "fission-console/internal/model" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/client-go/dynamic" ) // EnsureEnvironment создаёт Environment CRD для языка lang в namespace ns если не существует. // Возвращает имя environment (например "console-python-env"). // // Lazy creation: environments создаются только когда пользователь создаёт первую функцию // на конкретном языке. Это экономит ресурсы — Fission pool pods поднимаются только // под те языки которые реально используются, а не все сразу при создании namespace. func EnsureEnvironment(ctx context.Context, dyn dynamic.Interface, ns, lang string) (string, error) { langDef, ok := model.LangEnvMap[lang] if !ok { // Неизвестный язык — клиентская ошибка, возвращаем 400-совместимое сообщение return "", fmt.Errorf("unsupported language: %q", lang) } envName := "console-" + lang + "-env" // Проверяем существование — Get быстрее чем Create+IsAlreadyExists _, getErr := dyn.Resource(EnvironmentGVR).Namespace(ns).Get(ctx, envName, metav1.GetOptions{}) if getErr == nil { return envName, nil // уже существует — быстрый путь } if !apierrors.IsNotFound(getErr) { return "", fmt.Errorf("check environment %q: %w", envName, getErr) } // Строим и создаём Environment CRD env := buildLangEnvironment(envName, ns, langDef) if _, createErr := dyn.Resource(EnvironmentGVR).Namespace(ns).Create(ctx, env, metav1.CreateOptions{}); createErr != nil { if apierrors.IsAlreadyExists(createErr) { // Race condition: другой goroutine создал между Get и Create — это нормально return envName, nil } return "", fmt.Errorf("create environment %q: %w", envName, createErr) } log.Printf("ensureEnvironment: created %s/%s", ns, envName) return envName, nil } // CleanupEnvironmentIfUnused удаляет Environment CRD если ни одна функция в namespace // его не использует. Вызывается после удаления функции (handleDeleteFunction) // и при срабатывании reaper-а (runExpiryReap). // // Не блокирующий: ошибки логируются, не возвращаются вызывающему — это best-effort cleanup. // Fission увидит удаление Environment CRD и убьёт pool Deployment → поды умирают. func CleanupEnvironmentIfUnused(ctx context.Context, dyn dynamic.Interface, ns, envName string) { functions, err := dyn.Resource(FunctionGVR).Namespace(ns).List(ctx, metav1.ListOptions{}) if err != nil { log.Printf("cleanupEnvironmentIfUnused: list functions in %s: %v", ns, err) return } // Если хотя бы одна функция ссылается на этот environment — оставляем его for _, fn := range functions.Items { fnEnv, _, _ := unstructured.NestedString(fn.Object, "spec", "environment", "name") if fnEnv == envName { return // ещё используется } } // Ни одна функция не ссылается — удаляем if delErr := dyn.Resource(EnvironmentGVR).Namespace(ns).Delete(ctx, envName, metav1.DeleteOptions{}); delErr != nil && !apierrors.IsNotFound(delErr) { log.Printf("cleanupEnvironmentIfUnused: delete env %s/%s: %v", ns, envName, delErr) return } log.Printf("cleanupEnvironmentIfUnused: deleted unused env %s/%s", ns, envName) } // buildLangEnvironment строит unstructured.Unstructured объект Environment CRD. // Вся Fission-специфичная схема изолирована здесь — при обновлении Fission меняем только тут. func buildLangEnvironment(name, ns string, def model.LangEnvDef) *unstructured.Unstructured { // Version по умолчанию 3 (V2 protocol с async entrypoint). envVersion := int64(3) if def.Version != 0 { envVersion = int64(def.Version) } spec := map[string]any{ "version": envVersion, "runtime": map[string]any{ "image": def.Image, }, "poolsize": int64(1), } // BuilderImage задан только для Go — остальные языки интерпретируемые, // им builder не нужен: код передаётся напрямую в deployment.literal. if def.BuilderImage != "" { spec["builder"] = map[string]any{ "image": def.BuilderImage, "command": "build", } } return &unstructured.Unstructured{Object: map[string]any{ "apiVersion": "fission.io/v1", "kind": "Environment", "metadata": map[string]any{ "name": name, "namespace": ns, }, "spec": spec, }} }