restructure: console→client-console, add admin-console skeleton, move docs to doc/
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user