117 lines
5.0 KiB
Go
117 lines
5.0 KiB
Go
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,
|
||
}}
|
||
}
|