refactor: split main.go into packages (internal/api, internal/fission, internal/runtime, cmd/server)

This commit is contained in:
Naeel
2026-04-25 23:53:20 +03:00
parent 1f4de2ec15
commit 60646af0c8
20 changed files with 2799 additions and 1 deletions
+117
View File
@@ -0,0 +1,117 @@
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).
// Исключение: perl-env поддерживает только V1 protocol → version=1.
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,
}}
}