restructure: console→client-console, add admin-console skeleton, move docs to doc/

This commit is contained in:
“Naeel”
2026-05-25 09:48:55 +04:00
parent 3e36ff13a8
commit 34a8068da5
110 changed files with 615 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
// Package fission содержит константы GVR и вспомогательные функции
// для работы с Fission CRDs через Kubernetes dynamic client.
package fission
import "k8s.io/apimachinery/pkg/runtime/schema"
// GroupVersionResource константы для всех Fission CRD и core K8s ресурсов.
// Вынесены сюда чтобы не дублировать между пакетами и легко обновить при смене версии API.
var (
EnvironmentGVR = schema.GroupVersionResource{Group: "fission.io", Version: "v1", Resource: "environments"}
PackageGVR = schema.GroupVersionResource{Group: "fission.io", Version: "v1", Resource: "packages"}
FunctionGVR = schema.GroupVersionResource{Group: "fission.io", Version: "v1", Resource: "functions"}
HTTPTrigGVR = schema.GroupVersionResource{Group: "fission.io", Version: "v1", Resource: "httptriggers"}
TimeTrigGVR = schema.GroupVersionResource{Group: "fission.io", Version: "v1", Resource: "timetriggers"}
MQTrigGVR = schema.GroupVersionResource{Group: "fission.io", Version: "v1", Resource: "messagequeuetriggers"}
KWTrigGVR = schema.GroupVersionResource{Group: "fission.io", Version: "v1", Resource: "kuberneteswatchtriggers"}
NamespaceGVR = schema.GroupVersionResource{Group: "", Version: "v1", Resource: "namespaces"}
DeploymentGVR = schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}
)
@@ -0,0 +1,127 @@
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
existing, getErr := dyn.Resource(EnvironmentGVR).Namespace(ns).Get(ctx, envName, metav1.GetOptions{})
if getErr == nil {
// Уже существует — проверяем образ. Если устарел — обновляем.
currentImage, _, _ := unstructured.NestedString(existing.Object, "spec", "runtime", "image")
if currentImage != langDef.Image {
if patchErr := unstructured.SetNestedField(existing.Object, langDef.Image, "spec", "runtime", "image"); patchErr == nil {
if _, updateErr := dyn.Resource(EnvironmentGVR).Namespace(ns).Update(ctx, existing, metav1.UpdateOptions{}); updateErr != nil {
log.Printf("ensureEnvironment: update image %s/%s: %v", ns, envName, updateErr)
} else {
log.Printf("ensureEnvironment: updated image %s/%s: %s → %s", ns, envName, currentImage, langDef.Image)
}
}
}
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,
}}
}
@@ -0,0 +1,128 @@
// Package fission — чистый адаптер для Fission CRD API.
// Содержит только то что специфично для Fission и не зависит от облачной платформы:
// - SetupFissionNamespace: создание NS + label для Layer 1 NSWatcher + Fission ServiceAccounts + RoleBindings
// - EnsureEnvironment, CleanupEnvironmentIfUnused: управление Fission Environment CRD
// - GVR константы (client.go)
//
// Облачно-специфичные вещи (ResourceQuota, LimitRange, NetworkPolicy, NSManager) — в пакете cloud.
package fission
import (
"context"
"fmt"
"log"
"os"
"strings"
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/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
)
// SetupFissionNamespace создаёт namespace с нужными метками и регистрирует в нём
// все Fission ServiceAccounts и RoleBindings. Идемпотентно.
//
// Метки на Namespace:
// - managed-by=fission-console — для фильтрации наших NS в cloud-слое
// - fission.io/managed=true — для Layer 1 NSWatcher в executor (auto-discovery)
//
// ServiceAccounts:
// - fission-fetcher, fission-builder — нужны Fission pool pods в user NS
//
// RoleBindings (cluster-admin в рамках NS):
// - Для всех Fission system SA из FISSION_SYSTEM_NAMESPACE (default: fission)
// - Почему cluster-admin, а не admin: ClusterRole "admin" не включает fission.io/* CRD-группы,
// executor получает "RBAC escalation" при создании Role. cluster-admin в RoleBinding
// (не ClusterRoleBinding) безопасен — даёт полный доступ только внутри NS.
func SetupFissionNamespace(ctx context.Context, dyn dynamic.Interface, ns string) error {
// 1. Namespace
labels := map[string]any{
"managed-by": "fission-console",
"fission.io/managed": "true", // Layer 1: executor NSWatcher auto-discovers this NS
}
if strings.HasPrefix(ns, "fission-test-") {
labels["fission-console/env"] = "test"
}
nsObj := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "v1",
"kind": "Namespace",
"metadata": map[string]any{
"name": ns,
"labels": labels,
},
},
}
_, err := dyn.Resource(NamespaceGVR).Create(ctx, nsObj, metav1.CreateOptions{})
if err != nil && !apierrors.IsAlreadyExists(err) {
return fmt.Errorf("create namespace %s: %w", ns, err)
}
// 2. ServiceAccounts для Fission pool pods в user namespace.
// Fetcher sidecar требует fission-fetcher SA в том же NS где запускается.
saGVR := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "serviceaccounts"}
for _, saName := range []string{"fission-fetcher", "fission-builder"} {
saObj := &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "v1",
"kind": "ServiceAccount",
"metadata": map[string]any{"name": saName, "namespace": ns},
}}
_, saErr := dyn.Resource(saGVR).Namespace(ns).Create(ctx, saObj, metav1.CreateOptions{})
if saErr != nil && !apierrors.IsAlreadyExists(saErr) {
log.Printf("fission.SetupFissionNamespace: create SA %s/%s: %v", ns, saName, saErr)
}
}
// 3. RoleBindings для всех Fission system SA.
fissionSysNS := os.Getenv("FISSION_SYSTEM_NAMESPACE")
if fissionSysNS == "" {
fissionSysNS = "fission"
}
type rbDef struct {
name string
namespace string
binding string
}
bindings := []rbDef{
{name: "fission-executor", binding: "fission-executor-user-ns"},
{name: "fission-router", binding: "fission-router-user-ns"},
{name: "fission-buildermgr", binding: "fission-buildermgr-user-ns"},
{name: "fission-kubewatcher", binding: "fission-kubewatcher-user-ns"},
{name: "fission-timer", binding: "fission-timer-user-ns"},
{name: "fission-fetcher", binding: "fission-fetcher-system-user-ns"},
{name: "fission-builder", binding: "fission-builder-system-user-ns"},
// fetcher/builder также нужны локально (запускаются в user NS)
{name: "fission-fetcher", namespace: ns, binding: "fission-fetcher-local-user-ns"},
{name: "fission-builder", namespace: ns, binding: "fission-builder-local-user-ns"},
}
rbGVR := schema.GroupVersionResource{Group: "rbac.authorization.k8s.io", Version: "v1", Resource: "rolebindings"}
for _, rb := range bindings {
subjectNS := rb.namespace
if subjectNS == "" {
subjectNS = fissionSysNS
}
rbObj := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "rbac.authorization.k8s.io/v1",
"kind": "RoleBinding",
"metadata": map[string]any{"name": rb.binding, "namespace": ns},
"roleRef": map[string]any{
"apiGroup": "rbac.authorization.k8s.io",
"kind": "ClusterRole",
"name": "cluster-admin",
},
"subjects": []any{map[string]any{
"kind": "ServiceAccount", "name": rb.name, "namespace": subjectNS,
}},
},
}
_, rbErr := dyn.Resource(rbGVR).Namespace(ns).Create(ctx, rbObj, metav1.CreateOptions{})
if rbErr != nil && !apierrors.IsAlreadyExists(rbErr) {
log.Printf("fission.SetupFissionNamespace: create rolebinding %s/%s@%s: %v", ns, rb.name, subjectNS, rbErr)
}
}
return nil
}