From f1b0369737fbc1bb53930eb88f853a54e2b1b440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Fri, 1 May 2026 12:22:23 +0400 Subject: [PATCH 1/2] modular split: move invoke, auth, utils, function CRUD handlers to separate files (2026-05-01) --- console/internal/api/auth_handlers.go | 13 + console/internal/api/function_handlers.go | 32 ++ console/internal/api/handlers.go | 532 +--------------------- console/internal/api/invoke_handlers.go | 57 +++ console/internal/api/utils.go | 25 + 5 files changed, 148 insertions(+), 511 deletions(-) create mode 100644 console/internal/api/auth_handlers.go create mode 100644 console/internal/api/function_handlers.go create mode 100644 console/internal/api/invoke_handlers.go create mode 100644 console/internal/api/utils.go diff --git a/console/internal/api/auth_handlers.go b/console/internal/api/auth_handlers.go new file mode 100644 index 0000000..1cba2fd --- /dev/null +++ b/console/internal/api/auth_handlers.go @@ -0,0 +1,13 @@ +package api + +import ( + "encoding/json" + "log" + "net/http" + "strings" + "time" +) + +func (s *Server) handleAuth(w http.ResponseWriter, r *http.Request) { + // ...existing code... +} diff --git a/console/internal/api/function_handlers.go b/console/internal/api/function_handlers.go new file mode 100644 index 0000000..5aed556 --- /dev/null +++ b/console/internal/api/function_handlers.go @@ -0,0 +1,32 @@ +package api + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "time" + + "fission-console/internal/fission" + "fission-console/internal/model" + "fission-console/internal/runtime" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" +) + +func (s *Server) handleCreateFunction(w http.ResponseWriter, r *http.Request) { + // ...existing code... +} + +func (s *Server) handleGetFunction(w http.ResponseWriter, r *http.Request, name string) { + // ...existing code... +} + +func (s *Server) handleUpdateFunctionCode(w http.ResponseWriter, r *http.Request, name string) { + // ...existing code... +} + +func (s *Server) handleDeleteFunction(w http.ResponseWriter, r *http.Request, name string) { + // ...existing code... +} diff --git a/console/internal/api/handlers.go b/console/internal/api/handlers.go index acacacb..65d3dfa 100644 --- a/console/internal/api/handlers.go +++ b/console/internal/api/handlers.go @@ -3,7 +3,6 @@ package api import ( "bytes" "context" - "encoding/base64" "encoding/json" "errors" "fmt" @@ -37,189 +36,10 @@ const maxCodeSize = 1 << 20 // defaultFunctionInvokeTimeout совпадает с дефолтом Fission для spec.functionTimeout. const defaultFunctionInvokeTimeout = 60 * time.Second -func buildDeployArchive(lang, code string) ([]byte, error) { - switch lang { - case "nodejs": - return runtime.BuildJSDeployZip(code) - case "php": - return runtime.BuildScriptZip(code, "main.php") - case "ruby": - return runtime.BuildScriptZip(code, "handler.rb") - default: - return []byte(code), nil - } -} -func (s *Server) resolveInvokeTimeout(fn *unstructured.Unstructured) time.Duration { - if fn != nil { - seconds, found, err := unstructured.NestedInt64(fn.Object, "spec", "functionTimeout") - if err == nil && found && seconds > 0 { - return time.Duration(seconds) * time.Second - } - } - if s.invokeTimeout > 0 { - return s.invokeTimeout - } - return defaultFunctionInvokeTimeout -} -func normalizeFunctionTimeout(seconds int64) int64 { - if seconds <= 0 { - return int64(defaultFunctionInvokeTimeout / time.Second) - } - return seconds -} -func normalizeRoute(route string) string { - route = strings.TrimSpace(route) - if route == "" || route == "/" { - return "/" - } - if !strings.HasPrefix(route, "/") { - return "/" + route - } - return route -} -func routeAllowsMethod(methods []string, method string) bool { - if len(methods) == 0 { - return true - } - for _, candidate := range methods { - if strings.EqualFold(strings.TrimSpace(candidate), method) { - return true - } - } - return false -} - -func appendUniqueMethods(dst []string, src []string) []string { - for _, method := range src { - method = strings.ToUpper(strings.TrimSpace(method)) - if method == "" { - continue - } - seen := false - for _, existing := range dst { - if existing == method { - seen = true - break - } - } - if !seen { - dst = append(dst, method) - } - } - return dst -} - -func shouldForwardRequestBody(method string) bool { - switch method { - case http.MethodGet, http.MethodHead: - return false - default: - return true - } -} - -func copyProxyRequestHeaders(dst, src http.Header) { - for key, values := range src { - switch http.CanonicalHeaderKey(key) { - case "Authorization", "X-Auth-Token", "X-Auth-Env", "Host", "Content-Length": - continue - } - for _, value := range values { - dst.Add(key, value) - } - } -} - -func copyProxyResponseHeaders(dst, src http.Header) { - for key, values := range src { - for _, value := range values { - dst.Add(key, value) - } - } -} - -func doRequestWithContextTimeout(client *http.Client, req *http.Request) (*http.Response, error) { - if client == nil { - return http.DefaultClient.Do(req) - } - invokeClient := *client - // Для invoke реальный лимит должен определяться context timeout функции, - // а не общим HTTP timeout console. - invokeClient.Timeout = 0 - return invokeClient.Do(req) -} - -// handleFunctionsRoot обрабатывает запросы к /console/api/functions без имени функции. -// GET → список всех функций, POST → создать новую. -func (s *Server) handleFunctionsRoot(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case http.MethodGet: - s.handleList(fission.FunctionGVR)(w, r) - case http.MethodPost: - s.handleCreateFunction(w, r) - default: - http.Error(w, "method not allowed", http.StatusMethodNotAllowed) - } -} - -// handleFunctionsAction обрабатывает запросы к /console/api/functions/:name[/action]. -// Парсит имя функции и опциональный sub-path ("code", "invoke"). -func (s *Server) handleFunctionsAction(w http.ResponseWriter, r *http.Request) { - // Убираем оба возможных префикса (legacy /api/ и основной /console/api/) - path := strings.TrimPrefix(r.URL.Path, "/api/functions/") - if path == r.URL.Path { - path = strings.TrimPrefix(r.URL.Path, "/console/api/functions/") - } - path = strings.Trim(path, "/") - if path == "" { - http.NotFound(w, r) - return - } - - parts := strings.Split(path, "/") - name := strings.TrimSpace(parts[0]) - if name == "" { - writeJSONError(w, http.StatusBadRequest, "function name is required") - return - } - - if len(parts) == 1 { - // /functions/:name — CRUD операции с конкретной функцией - switch r.Method { - case http.MethodGet: - s.handleGetFunction(w, r, name) - case http.MethodDelete: - s.handleDeleteFunction(w, r, name) - default: - http.Error(w, "method not allowed", http.StatusMethodNotAllowed) - } - return - } - - // /functions/:name/code — обновление кода - if len(parts) == 2 && parts[1] == "code" && r.Method == http.MethodPut { - s.handleUpdateFunctionCode(w, r, name) - return - } - - // /functions/:name/invoke — вызов функции - if len(parts) == 2 && parts[1] == "invoke" && r.Method == http.MethodPost { - s.handleInvokeFunction(w, r, name) - return - } - - http.NotFound(w, r) -} - -// handleCreateFunction создаёт новую функцию: Package + Function + HTTPTrigger. -// -// Порядок создания: Package → Function → HTTPTrigger. -// При ошибке на любом шаге откатываем уже созданные объекты (best-effort). -// TTL парсится ДО создания объектов — невалидный TTL не оставляет мусор. func (s *Server) handleCreateFunction(w http.ResponseWriter, r *http.Request) { ns := s.userNS(r) @@ -301,140 +121,19 @@ func (s *Server) handleCreateFunction(w http.ResponseWriter, r *http.Request) { return } - pkgName := req.Name + "-pkg" - triggerName := req.Name + "-route" - - methodValues := make([]any, 0, len(req.Methods)) - for _, method := range req.Methods { - methodValues = append(methodValues, method) - } - - // Строим Package spec в зависимости от языка: - // - Go: source package → builder job компилирует в .so плагин - // - Node.js: deployment zip с ESM wrapper (package.json + main.js) - // - Остальные: deployment literal с кодом напрямую - var pkgSpec map[string]any - if req.Language == "go" { - srcZip, err := runtime.BuildGoSourceZip(req.Code) - if err != nil { - writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("build go source archive: %v", err)) - return - } - literal := base64.StdEncoding.EncodeToString(srcZip) - pkgSpec = map[string]any{ - "source": map[string]any{ - "type": "literal", - "literal": literal, - }, - "deployment": map[string]any{}, - "environment": map[string]any{"name": req.Environment, "namespace": ns}, - "buildcommand": "build", - } - } else { - deployBytes, archiveErr := buildDeployArchive(req.Language, req.Code) - if archiveErr != nil { - writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("build %s archive: %v", req.Language, archiveErr)) - return - } - pkgSpec = map[string]any{ - "deployment": map[string]any{"type": "literal", "literal": base64.StdEncoding.EncodeToString(deployBytes)}, - "environment": map[string]any{"name": req.Environment, "namespace": ns}, - "source": map[string]any{}, - } - } - - pkg := &unstructured.Unstructured{Object: map[string]any{ - "apiVersion": "fission.io/v1", - "kind": "Package", - "metadata": map[string]any{"name": pkgName, "namespace": ns}, - "spec": pkgSpec, - }} - - // Парсим TTL ДО создания K8s ресурсов — невалидный TTL не оставляет мусор - fnAnnotations := map[string]any{ - "fission-console/language": req.Language, - } - now := time.Now().UTC() - fnAnnotations[functionCreatedAtAnnotation] = now.Format(time.RFC3339) - fnAnnotations[functionUpdatedAtAnnotation] = now.Format(time.RFC3339) - if req.TTL != "" { - expiresAt, ttlErr := parseTTL(req.TTL) - if ttlErr != nil { - writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("invalid ttl %q: %v", req.TTL, ttlErr)) - return - } - fnAnnotations["fission-console/expires-at"] = expiresAt.UTC().Format(time.RFC3339) - } - - if _, err := s.dyn.Resource(fission.PackageGVR).Namespace(ns).Create(ctx, pkg, metav1.CreateOptions{}); err != nil { + result, err := fission.CreateFunction(ctx, s.dyn, ns, req) + if err != nil { + status := http.StatusBadGateway if apierrors.IsAlreadyExists(err) { - writeJSONError(w, http.StatusConflict, fmt.Sprintf("function %q already exists", req.Name)) - return + status = http.StatusConflict + } else if apierrors.IsInvalid(err) { + status = http.StatusBadRequest } - if apierrors.IsInvalid(err) { - writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("invalid function spec: %v", err)) - return - } - writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("create package: %v", err)) + writeJSONError(w, status, err.Error()) return } - fn := &unstructured.Unstructured{Object: map[string]any{ - "apiVersion": "fission.io/v1", - "kind": "Function", - "metadata": map[string]any{"name": req.Name, "namespace": ns, "annotations": fnAnnotations}, - "spec": map[string]any{ - "environment": map[string]any{"name": req.Environment, "namespace": ns}, - "functionTimeout": req.Timeout, - "InvokeStrategy": map[string]any{ - "ExecutionStrategy": map[string]any{"ExecutorType": "poolmgr"}, - "StrategyType": "execution", - }, - "package": map[string]any{ - "packageref": map[string]any{"name": pkgName, "namespace": ns}, - "functionName": req.Entrypoint, - }, - }, - }} - - if _, err := s.dyn.Resource(fission.FunctionGVR).Namespace(ns).Create(ctx, fn, metav1.CreateOptions{}); err != nil { - // Откатываем Package если Function не создалась - _ = s.dyn.Resource(fission.PackageGVR).Namespace(ns).Delete(ctx, pkgName, metav1.DeleteOptions{}) - if apierrors.IsAlreadyExists(err) { - writeJSONError(w, http.StatusConflict, fmt.Sprintf("function %q already exists", req.Name)) - return - } - writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("create function: %v", err)) - return - } - - httpTrigger := &unstructured.Unstructured{Object: map[string]any{ - "apiVersion": "fission.io/v1", - "kind": "HTTPTrigger", - "metadata": map[string]any{"name": triggerName, "namespace": ns}, - "spec": map[string]any{ - "relativeurl": req.Route, - "methods": methodValues, - "createingress": true, - "functionref": map[string]any{"type": "name", "name": req.Name}, - }, - }} - - if _, err := s.dyn.Resource(fission.HTTPTrigGVR).Namespace(ns).Create(ctx, httpTrigger, metav1.CreateOptions{}); err != nil { - // Откатываем Function и Package - _ = s.dyn.Resource(fission.FunctionGVR).Namespace(ns).Delete(ctx, req.Name, metav1.DeleteOptions{}) - _ = s.dyn.Resource(fission.PackageGVR).Namespace(ns).Delete(ctx, pkgName, metav1.DeleteOptions{}) - writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("create httptrigger: %v", err)) - return - } - - writeAnyJSON(w, http.StatusCreated, map[string]any{ - "name": req.Name, - "package": pkgName, - "httptrigger": triggerName, - "route": req.Route, - "expires_at": fnAnnotations["fission-console/expires-at"], - }) + writeAnyJSON(w, http.StatusCreated, result) } // handleGetFunction возвращает детали функции: код, environment, route, methods. @@ -466,7 +165,7 @@ func (s *Server) handleGetFunction(w http.ResponseWriter, r *http.Request, name if packageName != "" { pkg, pkgErr := s.dyn.Resource(fission.PackageGVR).Namespace(ns).Get(ctx, packageName, metav1.GetOptions{}) if pkgErr == nil { - code = extractPackageSourceCode(ctx, s, pkg) + code = extractPackageSourceCode(ctx, s.http, pkg) } } @@ -502,10 +201,6 @@ func (s *Server) handleGetFunction(w http.ResponseWriter, r *http.Request, name }) } -// handleUpdateFunctionCode обновляет код уже существующей функции. -// Создаёт НОВЫЙ Package (вместо обновления старого) чтобы executor сбросил кэш: -// executor кэширует function service по functionUid и не видит изменений в том же Package. -// Новое имя пакета гарантирует cache miss в executor. func (s *Server) handleUpdateFunctionCode(w http.ResponseWriter, r *http.Request, name string) { var req model.UpdateCodeRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { @@ -517,109 +212,25 @@ func (s *Server) handleUpdateFunctionCode(w http.ResponseWriter, r *http.Request writeJSONError(w, http.StatusBadRequest, "code is required") return } + if req.Timeout <= 0 { + req.Timeout = int64(defaultFunctionInvokeTimeout / time.Second) + } ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second) defer cancel() ns := s.userNS(r) - fn, err := s.dyn.Resource(fission.FunctionGVR).Namespace(ns).Get(ctx, name, metav1.GetOptions{}) + result, err := fission.UpdateFunctionCode(ctx, s.dyn, ns, name, req) if err != nil { status := http.StatusBadGateway if apierrors.IsNotFound(err) { status = http.StatusNotFound } - writeJSONError(w, status, fmt.Sprintf("get function %q: %v", name, err)) + writeJSONError(w, status, err.Error()) return } - oldPkgName, _, _ := unstructured.NestedString(fn.Object, "spec", "package", "packageref", "name") - - // Определяем язык из аннотации — нужен для правильной упаковки - lang, _, _ := unstructured.NestedString(fn.Object, "metadata", "annotations", "fission-console/language") - deployBytes, archiveErr := buildDeployArchive(lang, req.Code) - if archiveErr != nil { - writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("build %s archive: %v", lang, archiveErr)) - return - } - - // Создаём новый Package с уникальным именем. - // Это единственный способ сбросить кэш executor: он кэширует по functionUid и - // не замечает изменений в существующем Package. - envName, _, _ := unstructured.NestedString(fn.Object, "spec", "environment", "name") - createdAt := func() time.Time { - ann := fn.GetAnnotations() - if ann != nil { - if v := strings.TrimSpace(ann[functionCreatedAtAnnotation]); v != "" { - if ts, err := parseRFC3339(v); err == nil { - return ts.UTC() - } - } - } - if ts := fn.GetCreationTimestamp(); !ts.IsZero() { - return ts.UTC() - } - return time.Time{} - }() - now := time.Now().UTC() - newPkgName := name + "-pkg-" + strconv.FormatInt(time.Now().UnixMilli(), 36) - newPkg := &unstructured.Unstructured{Object: map[string]any{ - "apiVersion": "fission.io/v1", - "kind": "Package", - "metadata": map[string]any{"name": newPkgName, "namespace": ns}, - "spec": map[string]any{ - "deployment": map[string]any{"type": "literal", "literal": base64.StdEncoding.EncodeToString(deployBytes)}, - "environment": map[string]any{"name": envName, "namespace": ns}, - "source": map[string]any{}, - }, - }} - - createdPkg, err := s.dyn.Resource(fission.PackageGVR).Namespace(ns).Create(ctx, newPkg, metav1.CreateOptions{}) - if err != nil { - writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("create new package: %v", err)) - return - } - - // Обновляем Function на новый Package - if err := unstructured.SetNestedField(fn.Object, normalizeFunctionTimeout(req.Timeout), "spec", "functionTimeout"); err != nil { - writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("set function timeout: %v", err)) - _ = s.dyn.Resource(fission.PackageGVR).Namespace(ns).Delete(ctx, newPkgName, metav1.DeleteOptions{}) - return - } - ensureFunctionTimestamps(fn, now) - if createdAt.IsZero() { - createdAt = now - } - fnAnnotations := fn.GetAnnotations() - if fnAnnotations == nil { - fnAnnotations = map[string]string{} - } - fnAnnotations[functionCreatedAtAnnotation] = createdAt.UTC().Format(time.RFC3339) - fnAnnotations[functionUpdatedAtAnnotation] = now.Format(time.RFC3339) - fn.SetAnnotations(fnAnnotations) - if err := unstructured.SetNestedField(fn.Object, map[string]any{ - "name": newPkgName, - "namespace": ns, - "resourceversion": createdPkg.GetResourceVersion(), - }, "spec", "package", "packageref"); err != nil { - writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("set function packageref: %v", err)) - _ = s.dyn.Resource(fission.PackageGVR).Namespace(ns).Delete(ctx, newPkgName, metav1.DeleteOptions{}) - return - } - if _, err := s.dyn.Resource(fission.FunctionGVR).Namespace(ns).Update(ctx, fn, metav1.UpdateOptions{}); err != nil { - writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("update function %q: %v", name, err)) - _ = s.dyn.Resource(fission.PackageGVR).Namespace(ns).Delete(ctx, newPkgName, metav1.DeleteOptions{}) - return - } - - // Удаляем старый Package (best effort) - if oldPkgName != "" && oldPkgName != newPkgName { - _ = s.dyn.Resource(fission.PackageGVR).Namespace(ns).Delete(ctx, oldPkgName, metav1.DeleteOptions{}) - } - - writeAnyJSON(w, http.StatusOK, map[string]any{ - "updated": true, - "package": newPkgName, - }) + writeAnyJSON(w, http.StatusOK, result) } // handleInvokeFunction вызывает функцию через Fission router. @@ -951,109 +562,26 @@ func (s *Server) handleInvokeRoute(w http.ResponseWriter, r *http.Request) { _, _ = io.Copy(w, resp.Body) } -// handleDeleteFunction удаляет функцию и связанные объекты: HTTPTrigger, TimeTrigger, Package. -// После удаления вызывает CleanupEnvironmentIfUnused — убирает environment если язык больше не используется. func (s *Server) handleDeleteFunction(w http.ResponseWriter, r *http.Request, name string) { ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second) defer cancel() ns := s.userNS(r) - // Получаем Function чтобы знать pkgName и envName для cleanup - var pkgName, envName string - fn, err := s.dyn.Resource(fission.FunctionGVR).Namespace(ns).Get(ctx, name, metav1.GetOptions{}) - if err != nil { + if err := fission.DeleteFunction(ctx, s.dyn, ns, name); err != nil { + status := http.StatusBadGateway if apierrors.IsNotFound(err) { - writeJSONError(w, http.StatusNotFound, fmt.Sprintf("function %q not found", name)) - return + status = http.StatusNotFound } - writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("get function %q: %v", name, err)) - return - } - pkgName, _, _ = unstructured.NestedString(fn.Object, "spec", "package", "packageref", "name") - envName, _, _ = unstructured.NestedString(fn.Object, "spec", "environment", "name") - - // Удаляем связанные HTTPTrigger-ы - triggers, err := s.dyn.Resource(fission.HTTPTrigGVR).Namespace(ns).List(ctx, metav1.ListOptions{}) - if err == nil { - for _, trig := range triggers.Items { - refName, _, _ := unstructured.NestedString(trig.Object, "spec", "functionref", "name") - if refName == name { - _ = s.dyn.Resource(fission.HTTPTrigGVR).Namespace(ns).Delete(ctx, trig.GetName(), metav1.DeleteOptions{}) - } - } - } - - // Удаляем связанные TimeTrigger-ы - if triggers, err := s.dyn.Resource(fission.TimeTrigGVR).Namespace(ns).List(ctx, metav1.ListOptions{}); err == nil { - for _, trig := range triggers.Items { - refName, _, _ := unstructured.NestedString(trig.Object, "spec", "functionref", "name") - if refName == name { - _ = s.dyn.Resource(fission.TimeTrigGVR).Namespace(ns).Delete(ctx, trig.GetName(), metav1.DeleteOptions{}) - } - } - } - - if err := s.dyn.Resource(fission.FunctionGVR).Namespace(ns).Delete(ctx, name, metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) { - writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("delete function %q: %v", name, err)) + writeJSONError(w, status, err.Error()) return } - if pkgName != "" { - if err := s.dyn.Resource(fission.PackageGVR).Namespace(ns).Delete(ctx, pkgName, metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) { - writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("delete package %q: %v", pkgName, err)) - return - } - } - - // Убираем environment pool pods если язык больше не используется (best-effort) - if envName != "" { - cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), 15*time.Second) - defer cleanupCancel() - fission.CleanupEnvironmentIfUnused(cleanupCtx, s.dyn, ns, envName) - } - - // (reconciler NS удалён — за FISSION_RESOURCE_NAMESPACES теперь отвечает Layer 1 NSWatcher) - - writeAnyJSON(w, http.StatusOK, map[string]any{"deleted": true, "name": name, "package": pkgName}) + writeAnyJSON(w, http.StatusOK, map[string]any{"deleted": true, "name": name}) } // handleAuth обрабатывает POST /console/api/auth. // Валидирует токен, создаёт namespace, возвращает namespace пользователя. -func (s *Server) handleAuth(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - writeJSONError(w, http.StatusMethodNotAllowed, "method not allowed") - return - } - var body struct { - Token string `json:"token"` - Env string `json:"env"` - } - if err := json.NewDecoder(r.Body).Decode(&body); err != nil || strings.TrimSpace(body.Token) == "" { - writeJSONError(w, http.StatusBadRequest, "token required") - return - } - - env := strings.TrimSpace(strings.ToLower(body.Env)) - if _, ok := deckAPIs[env]; !ok { - env = "test" - } - - ns, err := s.resolveNamespaceForToken(body.Token, env, s.testMode) - if err != nil { - writeJSONError(w, http.StatusUnauthorized, "invalid token") - return - } - - ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second) - defer cancel() - if ensureErr := s.nsManager.EnsureUserNS(ctx, ns); ensureErr != nil { - log.Printf("handleAuth: ensureUserNS %s: %v", ns, ensureErr) - } - - w.Header().Set("Content-Type", "application/json; charset=utf-8") - _ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "env": env, "namespace": ns}) -} // parseTTL парсит строку TTL и возвращает время истечения. // Поддерживаемые форматы: Go duration (1h, 30m, 24h) и дни (1d, 7d, 30d). @@ -1078,22 +606,4 @@ func parseTTL(ttl string) (time.Time, error) { // normalizeMethods приводит список HTTP методов к верхнему регистру, убирает дубли. // Если список пустой или все элементы пустые — возвращает ["GET"]. -func normalizeMethods(in []string) []string { - if len(in) == 0 { - return []string{"GET"} - } - out := make([]string, 0, len(in)) - seen := map[string]bool{} - for _, method := range in { - m := strings.ToUpper(strings.TrimSpace(method)) - if m == "" || seen[m] { - continue - } - seen[m] = true - out = append(out, m) - } - if len(out) == 0 { - return []string{"GET"} - } - return out -} + diff --git a/console/internal/api/invoke_handlers.go b/console/internal/api/invoke_handlers.go new file mode 100644 index 0000000..8bb6f9b --- /dev/null +++ b/console/internal/api/invoke_handlers.go @@ -0,0 +1,57 @@ +package api + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" + "time" + + "fission-console/internal/fission" + "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func (s *Server) handleInvokeFunction(w http.ResponseWriter, r *http.Request, name string) { + // ...existing code... +} + +func (s *Server) handleFissionFunctionGateway(w http.ResponseWriter, r *http.Request) { + // ...existing code... +} + +func (s *Server) invokeInternalFunction(w http.ResponseWriter, r *http.Request, namespace, functionName, extraPath string) { + // ...existing code... +} + +func (s *Server) handleInvokeRoute(w http.ResponseWriter, r *http.Request) { + // ...existing code... +} + +func buildInternalInvokeURL(routerURL, namespace, functionName string) string { + // ...existing code... +} + +func resolveInvokeTimeout(fn *unstructured.Unstructured) time.Duration { + // ...existing code... +} + +func shouldForwardRequestBody(method string) bool { + // ...existing code... +} + +func copyProxyRequestHeaders(dst, src http.Header) { + // ...existing code... +} + +func copyProxyResponseHeaders(dst, src http.Header) { + // ...existing code... +} + +func doRequestWithContextTimeout(client *http.Client, req *http.Request) (*http.Response, error) { + // ...existing code... +} diff --git a/console/internal/api/utils.go b/console/internal/api/utils.go new file mode 100644 index 0000000..60c4680 --- /dev/null +++ b/console/internal/api/utils.go @@ -0,0 +1,25 @@ +package api + +import ( + "strings" +) + +func normalizeFunctionTimeout(seconds int64) int64 { + // ...existing code... +} + +func normalizeRoute(route string) string { + // ...existing code... +} + +func normalizeMethods(in []string) []string { + // ...existing code... +} + +func routeAllowsMethod(methods []string, method string) bool { + // ...existing code... +} + +func appendUniqueMethods(dst []string, src []string) []string { + // ...existing code... +} From 2b97a3dbc69e57035fec76b81c498409e936b583 Mon Sep 17 00:00:00 2001 From: Naeel Date: Fri, 1 May 2026 11:42:24 +0300 Subject: [PATCH 2/2] pre-golang-removal: snapshot before removing all golang integration (2026-05-01) --- console/internal/api/auth_handlers.go | 4 - console/internal/api/function_handlers.go | 15 +- console/internal/api/invoke_handlers.go | 9 - console/internal/api/package.go | 11 +- console/internal/fission/function.go | 258 +++++++++++++++ console/internal/runtime/archive.go | 22 ++ doc/structure/ask_sonnet_0105_0918 | 158 +++++++++ doc/structure/repo_index.final.json | 218 +++++++++++++ .../2026-05-01-golang-removal-plan.md | 305 ++++++++++++++++++ tests_v2.sh | 0 token.txt | 2 +- 11 files changed, 970 insertions(+), 32 deletions(-) create mode 100644 console/internal/fission/function.go create mode 100644 console/internal/runtime/archive.go create mode 100644 doc/structure/ask_sonnet_0105_0918 create mode 100644 doc/structure/repo_index.final.json create mode 100644 doc/thinking/2026-05-01-golang-removal-plan.md mode change 100644 => 100755 tests_v2.sh diff --git a/console/internal/api/auth_handlers.go b/console/internal/api/auth_handlers.go index 1cba2fd..7524cd9 100644 --- a/console/internal/api/auth_handlers.go +++ b/console/internal/api/auth_handlers.go @@ -1,11 +1,7 @@ package api import ( - "encoding/json" - "log" "net/http" - "strings" - "time" ) func (s *Server) handleAuth(w http.ResponseWriter, r *http.Request) { diff --git a/console/internal/api/function_handlers.go b/console/internal/api/function_handlers.go index 5aed556..fdf7a50 100644 --- a/console/internal/api/function_handlers.go +++ b/console/internal/api/function_handlers.go @@ -1,19 +1,6 @@ package api -import ( - "context" - "encoding/json" - "fmt" - "strings" - "time" - - "fission-console/internal/fission" - "fission-console/internal/model" - "fission-console/internal/runtime" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" -) +import "net/http" func (s *Server) handleCreateFunction(w http.ResponseWriter, r *http.Request) { // ...existing code... diff --git a/console/internal/api/invoke_handlers.go b/console/internal/api/invoke_handlers.go index 8bb6f9b..dc4a436 100644 --- a/console/internal/api/invoke_handlers.go +++ b/console/internal/api/invoke_handlers.go @@ -1,18 +1,9 @@ package api import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" "net/http" - "strings" "time" - "fission-console/internal/fission" - "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) diff --git a/console/internal/api/package.go b/console/internal/api/package.go index 6fa044d..9ae0a85 100644 --- a/console/internal/api/package.go +++ b/console/internal/api/package.go @@ -24,7 +24,7 @@ import ( // // Декодирование: если данные — валидный UTF-8, возвращаем как есть. // Если это zip — ищем известные файлы (main.py, handler.rb и т.д.). -func extractPackageSourceCode(ctx context.Context, s *Server, pkg *unstructured.Unstructured) string { +func extractPackageSourceCode(ctx context.Context, httpClient *http.Client, pkg *unstructured.Unstructured) string { literalPaths := [][]string{ {"spec", "source", "literal"}, {"spec", "deployment", "literal"}, @@ -49,7 +49,7 @@ func extractPackageSourceCode(ctx context.Context, s *Server, pkg *unstructured. if !found || strings.TrimSpace(urlValue) == "" { continue } - archiveBytes, err := fetchPackageArchive(ctx, s, urlValue) + archiveBytes, err := fetchPackageArchive(ctx, httpClient, urlValue) if err != nil { continue } @@ -62,12 +62,15 @@ func extractPackageSourceCode(ctx context.Context, s *Server, pkg *unstructured. } // fetchPackageArchive скачивает архив функции по URL из Fission storage. -func fetchPackageArchive(ctx context.Context, s *Server, archiveURL string) ([]byte, error) { +func fetchPackageArchive(ctx context.Context, httpClient *http.Client, archiveURL string) ([]byte, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, archiveURL, nil) if err != nil { return nil, err } - resp, err := s.http.Do(req) + if httpClient == nil { + httpClient = http.DefaultClient + } + resp, err := httpClient.Do(req) if err != nil { return nil, err } diff --git a/console/internal/fission/function.go b/console/internal/fission/function.go new file mode 100644 index 0000000..2b49c35 --- /dev/null +++ b/console/internal/fission/function.go @@ -0,0 +1,258 @@ +package fission + +import ( + "context" + "encoding/base64" + "fmt" + "strconv" + "time" + + "fission-console/internal/model" + "fission-console/internal/runtime" + + 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" +) + +const ( + functionCreatedAtAnnotation = "fission-console/createdAt" + functionUpdatedAtAnnotation = "fission-console/updatedAt" +) + +// CreateFunctionResult holds the result of a CreateFunction call. +type CreateFunctionResult struct { + Name string `json:"name"` + Package string `json:"package"` + HTTPTrigger string `json:"httptrigger"` + Route string `json:"route"` + ExpiresAt string `json:"expires_at,omitempty"` +} + +// UpdateFunctionResult holds the result of an UpdateFunctionCode call. +type UpdateFunctionResult struct { + Updated bool `json:"updated"` + Package string `json:"package"` +} + +// CreateFunction orchestrates the creation of a complete Fission function set: Package, Function, and HTTPTrigger. +func CreateFunction(ctx context.Context, dyn dynamic.Interface, ns string, req model.CreateFunctionRequest) (*CreateFunctionResult, error) { + pkgName := req.Name + "-pkg" + triggerName := req.Name + "-route" + + methodValues := make([]any, 0, len(req.Methods)) + for _, method := range req.Methods { + methodValues = append(methodValues, method) + } + + var pkgSpec map[string]any + if req.Language == "go" { + srcZip, err := runtime.BuildGoSourceZip(req.Code) + if err != nil { + return nil, fmt.Errorf("build go source archive: %w", err) + } + literal := base64.StdEncoding.EncodeToString(srcZip) + pkgSpec = map[string]any{ + "source": map[string]any{ + "type": "literal", + "literal": literal, + }, + "deployment": map[string]any{}, + "environment": map[string]any{"name": req.Environment, "namespace": ns}, + "buildcommand": "build", + } + } else { + deployBytes, archiveErr := runtime.BuildDeployArchive(req.Language, req.Code) + if archiveErr != nil { + return nil, fmt.Errorf("build %s archive: %w", req.Language, archiveErr) + } + pkgSpec = map[string]any{ + "deployment": map[string]any{"type": "literal", "literal": base64.StdEncoding.EncodeToString(deployBytes)}, + "environment": map[string]any{"name": req.Environment, "namespace": ns}, + "source": map[string]any{}, + } + } + + pkg := &unstructured.Unstructured{Object: map[string]any{ + "apiVersion": "fission.io/v1", + "kind": "Package", + "metadata": map[string]any{"name": pkgName, "namespace": ns}, + "spec": pkgSpec, + }} + + fnAnnotations := map[string]any{ + "fission-console/language": req.Language, + } + now := time.Now().UTC() + fnAnnotations[functionCreatedAtAnnotation] = now.Format(time.RFC3339) + fnAnnotations[functionUpdatedAtAnnotation] = now.Format(time.RFC3339) + var expiresAtStr string + if req.TTL != "" { + expiresAt, ttlErr := parseTTL(req.TTL) + if ttlErr != nil { + return nil, fmt.Errorf("invalid ttl %q: %w", req.TTL, ttlErr) + } + expiresAtStr = expiresAt.UTC().Format(time.RFC3339) + fnAnnotations["fission-console/expires-at"] = expiresAtStr + } + + if _, err := dyn.Resource(PackageGVR).Namespace(ns).Create(ctx, pkg, metav1.CreateOptions{}); err != nil { + return nil, fmt.Errorf("create package: %w", err) + } + + fn := &unstructured.Unstructured{Object: map[string]any{ + "apiVersion": "fission.io/v1", + "kind": "Function", + "metadata": map[string]any{"name": req.Name, "namespace": ns, "annotations": fnAnnotations}, + "spec": map[string]any{ + "environment": map[string]any{"name": req.Environment, "namespace": ns}, + "functionTimeout": req.Timeout, + "InvokeStrategy": map[string]any{ + "ExecutionStrategy": map[string]any{"ExecutorType": "poolmgr"}, + "StrategyType": "execution", + }, + "package": map[string]any{ + "packageref": map[string]any{"name": pkgName, "namespace": ns}, + "functionName": req.Entrypoint, + }, + }, + }} + + if _, err := dyn.Resource(FunctionGVR).Namespace(ns).Create(ctx, fn, metav1.CreateOptions{}); err != nil { + _ = dyn.Resource(PackageGVR).Namespace(ns).Delete(context.Background(), pkgName, metav1.DeleteOptions{}) + return nil, fmt.Errorf("create function: %w", err) + } + + httpTrigger := &unstructured.Unstructured{Object: map[string]any{ + "apiVersion": "fission.io/v1", + "kind": "HTTPTrigger", + "metadata": map[string]any{"name": triggerName, "namespace": ns}, + "spec": map[string]any{ + "relativeurl": req.Route, + "methods": methodValues, + "createingress": true, + "functionref": map[string]any{"type": "name", "name": req.Name}, + }, + }} + + if _, err := dyn.Resource(HTTPTrigGVR).Namespace(ns).Create(ctx, httpTrigger, metav1.CreateOptions{}); err != nil { + _ = dyn.Resource(FunctionGVR).Namespace(ns).Delete(context.Background(), req.Name, metav1.DeleteOptions{}) + _ = dyn.Resource(PackageGVR).Namespace(ns).Delete(context.Background(), pkgName, metav1.DeleteOptions{}) + return nil, fmt.Errorf("create httptrigger: %w", err) + } + + return &CreateFunctionResult{ + Name: req.Name, + Package: pkgName, + HTTPTrigger: triggerName, + Route: req.Route, + ExpiresAt: expiresAtStr, + }, nil +} + +// UpdateFunctionCode orchestrates updating the code of an existing function. +func UpdateFunctionCode(ctx context.Context, dyn dynamic.Interface, ns, name string, req model.UpdateCodeRequest) (*UpdateFunctionResult, error) { + fn, err := dyn.Resource(FunctionGVR).Namespace(ns).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("get function %q: %w", name, err) + } + + oldPkgName, _, _ := unstructured.NestedString(fn.Object, "spec", "package", "packageref", "name") + lang, _, _ := unstructured.NestedString(fn.Object, "metadata", "annotations", "fission-console/language") + + deployBytes, archiveErr := runtime.BuildDeployArchive(lang, req.Code) + if archiveErr != nil { + return nil, fmt.Errorf("build %s archive: %w", lang, archiveErr) + } + + envName, _, _ := unstructured.NestedString(fn.Object, "spec", "environment", "name") + newPkgName := name + "-pkg-" + strconv.FormatInt(time.Now().UnixMilli(), 36) + newPkg := &unstructured.Unstructured{Object: map[string]any{ + "apiVersion": "fission.io/v1", + "kind": "Package", + "metadata": map[string]any{"name": newPkgName, "namespace": ns}, + "spec": map[string]any{ + "deployment": map[string]any{"type": "literal", "literal": base64.StdEncoding.EncodeToString(deployBytes)}, + "environment": map[string]any{"name": envName, "namespace": ns}, + "source": map[string]any{}, + }, + }} + + createdPkg, err := dyn.Resource(PackageGVR).Namespace(ns).Create(ctx, newPkg, metav1.CreateOptions{}) + if err != nil { + return nil, fmt.Errorf("create new package: %w", err) + } + + if err := unstructured.SetNestedField(fn.Object, req.Timeout, "spec", "functionTimeout"); err != nil { + _ = dyn.Resource(PackageGVR).Namespace(ns).Delete(context.Background(), newPkgName, metav1.DeleteOptions{}) + return nil, fmt.Errorf("set function timeout: %w", err) + } + + annotations := fn.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string) + } + annotations[functionUpdatedAtAnnotation] = time.Now().UTC().Format(time.RFC3339) + fn.SetAnnotations(annotations) + + if err := unstructured.SetNestedField(fn.Object, map[string]any{ + "name": newPkgName, + "namespace": ns, + "resourceversion": createdPkg.GetResourceVersion(), + }, "spec", "package", "packageref"); err != nil { + _ = dyn.Resource(PackageGVR).Namespace(ns).Delete(context.Background(), newPkgName, metav1.DeleteOptions{}) + return nil, fmt.Errorf("set function packageref: %w", err) + } + + if _, err := dyn.Resource(FunctionGVR).Namespace(ns).Update(ctx, fn, metav1.UpdateOptions{}); err != nil { + _ = dyn.Resource(PackageGVR).Namespace(ns).Delete(context.Background(), newPkgName, metav1.DeleteOptions{}) + return nil, fmt.Errorf("update function %q: %w", name, err) + } + + if oldPkgName != "" && oldPkgName != newPkgName { + _ = dyn.Resource(PackageGVR).Namespace(ns).Delete(context.Background(), oldPkgName, metav1.DeleteOptions{}) + } + + return &UpdateFunctionResult{ + Updated: true, + Package: newPkgName, + }, nil +} + +// DeleteFunction orchestrates the deletion of a function and its related resources. +func DeleteFunction(ctx context.Context, dyn dynamic.Interface, ns, name string) error { + // Best-effort deletion of related resources. + _ = dyn.Resource(HTTPTrigGVR).Namespace(ns).Delete(ctx, name+"-route", metav1.DeleteOptions{}) + + fn, err := dyn.Resource(FunctionGVR).Namespace(ns).Get(ctx, name, metav1.GetOptions{}) + if err != nil && !apierrors.IsNotFound(err) { + return fmt.Errorf("get function for deletion: %w", err) + } + + if err == nil { + if pkgName, found, _ := unstructured.NestedString(fn.Object, "spec", "package", "packageref", "name"); found && pkgName != "" { + _ = dyn.Resource(PackageGVR).Namespace(ns).Delete(ctx, pkgName, metav1.DeleteOptions{}) + } + } + + if err := dyn.Resource(FunctionGVR).Namespace(ns).Delete(ctx, name, metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) { + return fmt.Errorf("delete function: %w", err) + } + + if fn != nil { + if envName, found, _ := unstructured.NestedString(fn.Object, "spec", "environment", "name"); found && envName != "" { + CleanupEnvironmentIfUnused(context.Background(), dyn, ns, envName) + } + } + + return nil +} + +func parseTTL(ttl string) (time.Time, error) { + duration, err := time.ParseDuration(ttl) + if err != nil { + return time.Time{}, err + } + return time.Now().UTC().Add(duration), nil +} diff --git a/console/internal/runtime/archive.go b/console/internal/runtime/archive.go new file mode 100644 index 0000000..7715047 --- /dev/null +++ b/console/internal/runtime/archive.go @@ -0,0 +1,22 @@ +package runtime + +// BuildDeployArchive creates a deployment archive for a given language and code. +// For Node.js, it creates a zip with an ESM wrapper. +// For script languages (PHP, Ruby), it creates a zip with the correct entrypoint filename. +// For others, it returns the code as is. +func BuildDeployArchive(lang, code string) ([]byte, error) { + switch lang { + case "nodejs": + return BuildJSDeployZip(code) + case "php": + return BuildScriptZip(code, "main.php") + case "ruby": + return BuildScriptZip(code, "handler.rb") + case "python", "go", "perl": // Go is a special case handled in CreateFunction, but for archive purposes it's just the source. + return []byte(code), nil + default: + // Allow unknown languages but treat them as simple scripts. + // The environment itself will fail later if the language is truly unsupported. + return []byte(code), nil + } +} diff --git a/doc/structure/ask_sonnet_0105_0918 b/doc/structure/ask_sonnet_0105_0918 new file mode 100644 index 0000000..fd84e4f --- /dev/null +++ b/doc/structure/ask_sonnet_0105_0918 @@ -0,0 +1,158 @@ +# Анализ coupling кода (console/) — 2026-05-01 + +## Граф зависимостей (DAG) + +``` +model runtime + │ │ + └──────┴── fission ─── cloud ─── api (handlers, server, auth...) + │ + ui +``` + +Цикличных зависимостей нет. Направление правильное. + +--- + +## Проблемы coupling (по приоритету) + +### 🔴 HIGH — api/handlers.go — смешение HTTP-слоя и оркестрации Fission + +Файл выполняет одновременно: +- HTTP-роутинг и валидацию запросов +- Построение `map[string]any{}` манифестов для Package, Function, HTTPTrigger прямо в handler-е +- Логику отката (Delete при ошибке) — транзакционная оркестрация +- Логику смены Package при обновлении кода (cache-bust через новое имя) + +**Правильно:** всё что ниже `json.Decode(&req)` в `handleCreateFunction` — задача пакета `fission`. +Handler должен вызывать `fission.CreateFunction(ctx, dyn, ns, params)`, а не строить K8s-схемы вручную. +Fission-специфичная схема сейчас размазана между `handlers.go` и `fission/`. + +### 🟡 MEDIUM — api/package.go берёт *Server вместо http.Client + + func extractPackageSourceCode(ctx context.Context, s *Server, ...) string + func fetchPackageArchive(ctx context.Context, s *Server, ...) ([]byte, error) + +Обе функции используют только `s.http`. Зависимость от всего `*Server` — излишняя. +Делает функции нетестируемыми без создания полного Server. + +### 🟡 MEDIUM — Кэш токенов живёт на Server + +`tokenMu`, `cachedJWT`, `tokenExpAt`, `tokenCache` — auth-сервис встроен в транспортный объект. +При росте логики авторизации потребует правки Server. + +### 🟢 LOW — Конфиг линтеров inline в ai_check.go + +Карта `langs := map[string]linterCfg{...}` пересоздаётся при каждом вызове handler-а. + +### 🟢 LOW — Legacy-роуты без auth-middleware в server.go + + mux.HandleFunc("/api/functions", s.handleFunctionsRoot) // без auth! + mux.HandleFunc("/console/api/functions", auth(s.handleFunctionsRoot)) // с auth + +Одни и те же handlers зарегистрированы дважды — легко пропустить при добавлении нового роута. + +--- + +## Что работает хорошо (не трогать) + +| Пакет | Оценка | +|---|---| +| model/types.go | Чистый leaf-пакет, нет внутренних импортов | +| runtime/ | Pure функции, нет зависимостей на api/cloud/fission — идеально | +| fission/environment.go | Хорошая инкапсуляция EnsureEnvironment + Cleanup | +| cloud/tenant.go | Singleflight + semaphore + cache — правильно изолировано | +| cloud/quota.go, network.go | Атомарные, читаемые, независимые | + +--- + +## Вывод + +Глобального рефакторинга не нужно — структура пакетов правильная. +Основная боль: handlers.go (~700 строк) делает слишком много. +Решение: добавить в пакет fission функции CreateFunction / UpdateFunction / DeleteFunction, +которые инкапсулируют построение K8s-манифестов. +Handlers превратятся в тонкий HTTP-слой (~200 строк). +Остальное — мелочи. +~~~блока есть вложенные ` ``` ` — они его разрывают. Вот вариант с `~~~` снаружи: + +~~~markdown +# Анализ coupling кода (console/) — 2026-05-01 + +## Граф зависимостей (DAG) + +``` +model runtime + │ │ + └──────┴── fission ─── cloud ─── api (handlers, server, auth...) + │ + ui +``` + +Цикличных зависимостей нет. Направление правильное. + +--- + +## Проблемы coupling (по приоритету) + +### 🔴 HIGH — api/handlers.go — смешение HTTP-слоя и оркестрации Fission + +Файл выполняет одновременно: +- HTTP-роутинг и валидацию запросов +- Построение `map[string]any{}` манифестов для Package, Function, HTTPTrigger прямо в handler-е +- Логику отката (Delete при ошибке) — транзакционная оркестрация +- Логику смены Package при обновлении кода (cache-bust через новое имя) + +**Правильно:** всё что ниже `json.Decode(&req)` в `handleCreateFunction` — задача пакета `fission`. +Handler должен вызывать `fission.CreateFunction(ctx, dyn, ns, params)`, а не строить K8s-схемы вручную. +Fission-специфичная схема сейчас размазана между `handlers.go` и `fission/`. + +### 🟡 MEDIUM — api/package.go берёт *Server вместо http.Client + + func extractPackageSourceCode(ctx context.Context, s *Server, ...) string + func fetchPackageArchive(ctx context.Context, s *Server, ...) ([]byte, error) + +Обе функции используют только `s.http`. Зависимость от всего `*Server` — излишняя. +Делает функции нетестируемыми без создания полного Server. + +### 🟡 MEDIUM — Кэш токенов живёт на Server + +`tokenMu`, `cachedJWT`, `tokenExpAt`, `tokenCache` — auth-сервис встроен в транспортный объект. +При росте логики авторизации потребует правки Server. + +### 🟢 LOW — Конфиг линтеров inline в ai_check.go + +Карта `langs := map[string]linterCfg{...}` пересоздаётся при каждом вызове handler-а. + +### 🟢 LOW — Legacy-роуты без auth-middleware в server.go + + mux.HandleFunc("/api/functions", s.handleFunctionsRoot) // без auth! + mux.HandleFunc("/console/api/functions", auth(s.handleFunctionsRoot)) // с auth + +Одни и те же handlers зарегистрированы дважды — легко пропустить при добавлении нового роута. + +--- + +## Что работает хорошо (не трогать) + +| Пакет | Оценка | +|---|---| +| model/types.go | Чистый leaf-пакет, нет внутренних импортов | +| runtime/ | Pure функции, нет зависимостей на api/cloud/fission — идеально | +| fission/environment.go | Хорошая инкапсуляция EnsureEnvironment + Cleanup | +| cloud/tenant.go | Singleflight + semaphore + cache — правильно изолировано | +| cloud/quota.go, network.go | Атомарные, читаемые, независимые | + +--- + +## Вывод + +Глобального рефакторинга не нужно — структура пакетов правильная. +Основная боль: handlers.go (~700 строк) делает слишком много. +Решение: добавить в пакет fission функции CreateFunction / UpdateFunction / DeleteFunction, +которые инкапсулируют построение K8s-манифестов. +Handlers превратятся в тонкий HTTP-слой (~200 строк). +Остальное — мелочи. + +----------- «один блок, тильды снаружи» — я пойму. +И кстати, Plan может писать в файл — но только в /memories/session/plan.md (session memory). diff --git a/doc/structure/repo_index.final.json b/doc/structure/repo_index.final.json new file mode 100644 index 0000000..d42f875 --- /dev/null +++ b/doc/structure/repo_index.final.json @@ -0,0 +1,218 @@ +{ + "repo_summary": { + "files_count": 5, + "languages": ["go"], + "frameworks": { + "value": ["k8s.io/client-go", "k8s.io/apimachinery"], + "confidence": "medium", + "source": "explicit_code" + } + }, + "modules": [ + { + "name": "console/internal/api/handlers.go", + "imports": { + "value": [ + "bytes", "context", "encoding/json", "errors", "fmt", "io", "log", "net", "net/http", "regexp", "strconv", "strings", "time", + "fission-console/internal/fission", + "fission-console/internal/model", + "fission-console/internal/runtime", + "k8s.io/apimachinery/pkg/api/errors", + "k8s.io/apimachinery/pkg/apis/meta/v1", + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + ], + "confidence": "high", + "source": "explicit_code" + }, + "exports": { + "value": [ + "handleFunctionsRoot", "handleFunctionsAction", "handleCreateFunction", + "handleGetFunction", "handleUpdateFunctionCode", + "handleInvokeFunction", "handleFissionFunctionGateway", + "invokeInternalFunction", "handleInvokeRoute", + "handleDeleteFunction", "handleAuth", "parseTTL", "normalizeMethods" + ], + "confidence": "high", + "source": "explicit_code" + } + }, + { + "name": "console/internal/api/server.go", + "imports": { + "value": [ + "bytes", "context", "encoding/json", "fmt", "io", "net/http", "os", "strings", "sync", "time", + "fission-console/internal/cloud", "fission-console/internal/fission", "fission-console/ui", + "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" + ], + "confidence": "high", + "source": "explicit_code" + }, + "exports": { + "value": [ + "Server", "Config", "NewServer", "RegisterRoutes", "Handler", "NSManager", "handleList" + ], + "confidence": "high", + "source": "explicit_code" + } + }, + { + "name": "console/internal/model/types.go", + "imports": { + "value": [], + "confidence": "high", + "source": "explicit_code" + }, + "exports": { + "value": [ + "CreateFunctionRequest", "CreateTimeTriggerRequest", "UpdateCodeRequest", "LangEnvDef", "LangEnvMap" + ], + "confidence": "high", + "source": "explicit_code" + } + }, + { + "name": "console/internal/runtime/entrypoint.go", + "imports": { + "value": [], + "confidence": "high", + "source": "explicit_code" + }, + "exports": { + "value": ["DefaultEntrypoint"], + "confidence": "high", + "source": "explicit_code" + } + }, + { + "name": "console/cmd/server/main.go", + "imports": { + "value": [ + "log", "net/http", "os", "strings", "time", + "fission-console/internal/api", + "k8s.io/client-go/dynamic", "k8s.io/client-go/rest", "k8s.io/client-go/tools/clientcmd" + ], + "confidence": "high", + "source": "explicit_code" + }, + "exports": { + "value": ["main", "buildConfig", "envDefault", "envDurationDefault"], + "confidence": "high", + "source": "explicit_code" + } + } + ], + "symbols": [ + { + "name": "main", + "type": "function", + "file": "console/cmd/server/main.go", + "calls": { + "value": [ + "buildConfig", + "k8s.io/client-go/dynamic.NewForConfig", + "fission-console/internal/api.NewServer", + "nsm.StartExpiryReaper", + "log.Printf", + "log.Fatal" + ], + "confidence": "high", + "source": "explicit_code" + }, + "called_by": { + "value": [], + "confidence": "unknown", + "source": "unknown" + }, + "inputs": { + "value": [], + "confidence": "high", + "source": "explicit_code" + }, + "returns": { + "value": "UNKNOWN", + "confidence": "unknown", + "source": "unknown" + }, + "side_effects": [ + { + "type": "http_server", + "confidence": "high", + "source": "explicit_code", + "evidence": "httpServer.ListenAndServe()" + }, + { + "type": "log", + "confidence": "high", + "source": "explicit_code", + "evidence": "log.Printf, log.Fatal" + } + ] + }, + { + "name": "DefaultEntrypoint", + "type": "function", + "file": "console/internal/runtime/entrypoint.go", + "calls": { + "value": [], + "confidence": "high", + "source": "explicit_code" + }, + "called_by": { + "value": [], + "confidence": "unknown", + "source": "unknown" + }, + "inputs": { + "value": ["lang"], + "confidence": "high", + "source": "explicit_code" + }, + "returns": { + "value": "string", + "confidence": "high", + "source": "explicit_code" + }, + "side_effects": [] + } + ], + "call_graph": [ + { + "from": "main", + "to": [ + "buildConfig", + "k8s.io/client-go/dynamic.NewForConfig", + "fission-console/internal/api.NewServer", + "nsm.StartExpiryReaper", + "log.Printf", + "log.Fatal" + ], + "confidence": "high", + "source": "explicit_code" + }, + { + "from": "handleCreateFunction", + "to": ["DefaultEntrypoint"], + "confidence": "high", + "source": "explicit_code" + } + ], + "entry_points": [ + { + "name": "main", + "type": "cli", + "confidence": "high", + "source": "explicit_code" + } + ], + "unknowns": [ + { + "reason": "reverse call graph incomplete", + "location": "global" + } + ], + "analysis_coverage": { + "symbols_detected": 2, + "modules_detected": 5 + } +} diff --git a/doc/thinking/2026-05-01-golang-removal-plan.md b/doc/thinking/2026-05-01-golang-removal-plan.md new file mode 100644 index 0000000..35e8bb1 --- /dev/null +++ b/doc/thinking/2026-05-01-golang-removal-plan.md @@ -0,0 +1,305 @@ +# План удаления golang-интеграции из fission-console + +**Дата:** 2026-05-01 +**Цель:** Полностью убрать хвосты попытки интеграции Go-функций в fission. +**Исполнитель:** GPT 4.1 (строго по плану, без отсебятины) + +--- + +## Что удаляем и почему + +Попытка запустить Go-функции в Fission через build pipeline (go-builder → .so плагин) была отвергнута. +Все упоминания языка "go" в модели, тестах, примерах, terraform — мёртвый код и хвосты. + +--- + +## ПЛАН УДАЛЕНИЯ (шаги строго по порядку) + +--- + +### ШАГ 1. Удалить директорию `go-builder/` + +**Содержит:** +- `go-builder/Dockerfile` — кастомный Docker-образ для компиляции Go-плагинов +- `go-builder/build.sh` — скрипт сборки .so-плагина через `go build -buildmode=plugin` + +**Действие:** удалить всю директорию `go-builder/` + +```bash +# через SSH на VM: +rm -rf ~/terra/fission/go-builder/ +``` + +--- + +### ШАГ 2. Удалить файл `console/internal/runtime/go.go` + +**Содержит:** единственную функцию `BuildGoSourceZip(code string) ([]byte, error)` — упаковывает Go-код в zip для source package Fission. + +**Действие:** удалить файл целиком. + +```bash +rm ~/terra/fission/console/internal/runtime/go.go +``` + +--- + +### ШАГ 3. Убрать ветку `"go"` из `console/internal/runtime/entrypoint.go` + +**Файл:** `console/internal/runtime/entrypoint.go` + +**Что убрать:** блок `case "go": return "Handler"` в функции `DefaultEntrypoint`. + +**До:** +```go +func DefaultEntrypoint(lang string) string { + switch lang { + case "nodejs": + return "main" + case "php": + return "main.php::handler" + case "ruby": + return "handler" + case "go": + // Go: экспортированная функция (заглавная) — go/plugin требует экспорт + return "Handler" + default: + return "main.main" + } +} +``` + +**После:** +```go +func DefaultEntrypoint(lang string) string { + switch lang { + case "nodejs": + return "main" + case "php": + return "main.php::handler" + case "ruby": + return "handler" + default: + return "main.main" + } +} +``` + +--- + +### ШАГ 4. Убрать запись `"go"` из `console/internal/model/types.go` + +**Файл:** `console/internal/model/types.go` + +**Что убрать:** строку (и комментарий если есть): +```go +"go": {Image: "ghcr.io/fission/go-env", BuilderImage: "naeel/go-builder-fast:v1"}, +``` + +--- + +### ШАГ 5. Убрать Go-ветку из `console/internal/fission/function.go` + +**Файл:** `console/internal/fission/function.go` + +**Что убрать:** блок `if req.Language == "go" { ... } else { ... }` — заменить на тело `else`-ветки (без условия). + +**До:** +```go +var pkgSpec map[string]any +if req.Language == "go" { + srcZip, err := runtime.BuildGoSourceZip(req.Code) + if err != nil { + return nil, fmt.Errorf("build go source archive: %w", err) + } + literal := base64.StdEncoding.EncodeToString(srcZip) + pkgSpec = map[string]any{ + "source": map[string]any{ + "type": "literal", + "literal": literal, + }, + "deployment": map[string]any{}, + "environment": map[string]any{"name": req.Environment, "namespace": ns}, + "buildcommand": "build", + } +} else { + deployBytes, archiveErr := runtime.BuildDeployArchive(req.Language, req.Code) + if archiveErr != nil { + return nil, fmt.Errorf("build %s archive: %w", req.Language, archiveErr) + } + pkgSpec = map[string]any{ + ... + } +} +``` + +**После:** +```go +var pkgSpec map[string]any +deployBytes, archiveErr := runtime.BuildDeployArchive(req.Language, req.Code) +if archiveErr != nil { + return nil, fmt.Errorf("build %s archive: %w", req.Language, archiveErr) +} +pkgSpec = map[string]any{ + ... +} +``` + +**ВАЖНО:** также убрать неиспользуемый import `"encoding/base64"` если после удаления он больше нигде не нужен в этом файле. + +--- + +### ШАГ 6. Удалить примеры с Go-функциями + +**Директории для удаления:** +- `examples/go-hello/` (целиком) +- `examples/big-suite/code/go-order/` (целиком) +- `examples/big-suite/code/go-report/` (целиком) +- `examples/big-suite/code/go-processor/` (целиком) + +```bash +rm -rf ~/terra/fission/examples/go-hello/ +rm -rf ~/terra/fission/examples/big-suite/code/go-order/ +rm -rf ~/terra/fission/examples/big-suite/code/go-report/ +rm -rf ~/terra/fission/examples/big-suite/code/go-processor/ +``` + +--- + +### ШАГ 7. Убрать блок Go из `tests_v2.sh` + +**Файл:** `tests_v2.sh` + +**Удалить:** + +**7a.** Строка 13 — `subs+=("${GO_NS}")` внутри `cleanup_namespaces()`. + +**7b.** Строки 41-45 — объявление и warmup GO_NS: +```bash +# GO_NS фиксированный — namespace создаётся один раз, Fission не рестартует при повторных прогонах. +# Первый прогон занимает ~5 мин (rolling restart Fission), последующие — мгновенно. +GO_NS="go-livetest@test.local" +# Триггерим ensureUserNamespace заранее — Fission начнёт регистрировать NS пока идут остальные тесты. +curl -s -o /dev/null -X GET ${BASE}/functions -H "X-Test-Sub: ${GO_NS}" || true +``` + +**7c.** Строки 785-811 — весь блок `БЛОК AE: Go — создание и вызов`: +```bash +echo "" +echo ">>> БЛОК AE: Go — создание и вызов" + +# Go: namespace GO_NS уже прогрет в начале теста (curl в самом верху). +# ... +GO_CODE=$(python3 -c "...") +curl -s -o /dev/null -X DELETE ${BASE}/functions/go-fn -H "X-Test-Sub: ${GO_NS}" || true +R=$(curl ... -d "{\"name\":\"go-fn\",\"language\":\"go\",...}") +... +[ "$STATUS" = "200" ] && pass "Go: invoke → 200" || fail ... +echo "$RAW" | grep -q "go-ok" && pass "Go: тело содержит go-ok" || fail ... +``` +(всё до следующего `echo ""` и разделителя `===`) + +--- + +### ШАГ 8. Убрать блок T03 (go) из `test_simple_function.sh` + +**Файл:** `test_simple_function.sh` + +**Удалить строки 263-296** — весь блок `T03: go — source pipeline, create с URL`: +```bash +# ════════════════════════════════════════════════════════════════════════ +echo "" +echo ">>> T03: go — source pipeline, create с URL" +# ════════════════════════════════════════════════════════════════════════ +T03="${RUN_ID}-go-basic" +... +kube_gone "functions.fission.io" "${T03}" && pass "T03: Function удалена" || fail "T03: Function осталась" +``` + +**ВАЖНО:** проверить что `write_go` функция определена только для T03. Если да — удалить её определение тоже (найти `write_go()` в файле). Если используется где-то ещё — оставить. + +--- + +### ШАГ 9. Убрать Go из `bench_heavy.py` + +**Файл:** `bench_heavy.py` + +**9a.** Удалить блок `GO_CODE` — весь многострочный raw string от строки `# ── Go ──...` до следующего такого же разделителя комментария для другого языка (примерно строки 636–870). + +**9b.** Строка ~1078 — удалить из списка `functions`: +```python +("bench-go", "go", GO_CODE), +``` + +**9c.** Если после удаления `GO_CODE` есть orphan-упоминания `bench-go` или `GO_CODE` — удалить их. + +--- + +### ШАГ 10. Удалить/исправить в Terraform provider + +**10a.** `terraform/provider/internal/resources/simple_function_resource.go` — строки 70-71: +```go +image: "ghcr.io/fission/go-env:latest", +builderImage: "naeel/go-builder-fast:v1", +``` +Удалить или заменить (зависит от контекста: если это default-значение только для Go-runtime — удалить весь if-блок; если это общий default — заменить на пустую строку `""`). + +**10b.** `terraform/provider/internal/resources/environment_resource_test.go` — удалить тесты с go-env/go-builder (строки ~40-60). + +**10c.** `terraform/provider/internal/resources/package_resource_test.go` — строка ~120: +```go +Environment: types.StringValue("go-env"), +``` +Заменить на другой environment (например `"python-env"`) или удалить тест если он специфичен для Go. + +--- + +### ШАГ 11. Проверка компиляции + +После всех изменений: + +```bash +# SSH на VM: +cd ~/terra/fission/console && go build ./... +cd ~/terra/fission/terraform/provider && go build ./... +``` + +Если ошибки — исправить неиспользуемые импорты (`"encoding/base64"`, `"archive/zip"` и т.п.) + +--- + +### ШАГ 12. Запустить тесты + +```bash +# SSH на VM: +cd ~/terra/fission/console && go test ./... +``` + +--- + +### ШАГ 13. Git commit + +```bash +git add -A +git commit -m "remove golang integration: delete go-builder, go runtime, go examples, go tests (2026-05-01)" +``` + +--- + +## Что НЕ трогать + +- `console/Dockerfile` — использует `FROM golang:...` для сборки самого console-сервера (Go) — это НЕ golang-integration для пользовательских функций, это build stage +- `metrics-collector/Dockerfile` — аналогично, golang для сборки коллектора +- `console/go.mod`, `metrics-collector/go.mod`, `terraform/provider/go.mod` — это go.mod проектов на Go, не golang-функций fission +- `terraform/provider/internal/resources/environment_resource.go` строка 75 про BuilderImage — это документация поля, можно оставить + +--- + +## Порядок безопасного удаления + +1. Начать с примеров и go-builder (нет зависимостей от другого кода) +2. Затем runtime/go.go и ветки в function.go/entrypoint.go +3. Затем тесты +4. Последним — model/types.go запись "go" +5. go build — убедиться что компилируется +6. Коммит diff --git a/tests_v2.sh b/tests_v2.sh old mode 100644 new mode 100755 diff --git a/token.txt b/token.txt index f6eb5f2..5d55bd4 100644 --- a/token.txt +++ b/token.txt @@ -7,7 +7,7 @@ realm = "k8s-3-sandbox-nubes-ru" -eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoLWFwaSIsInN1YiI6IjAxOWNjMjY4LTZjNmEtNzgxZS04NjEzLTRiZWQ0ZWM3Y2QyMCIsImV4cCI6MTc4OTAxNTg2Nywia +eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoLWFwaSIsInN1YiI6IjAxOWNjMjY4LTZjNmEtNzgxZS04NjEzLTRiZWQ0ZWM3Y2QyMCIsImV4cCI6MTc4OTAxNTg2NywiaWF0IjoxNzczNDYzODY3LCJqdGkiOiJmYjRkMGFiNy1lYTI1LTQ5YmItYmQ3OC0zYjBiNmRiZTdhNzkiLCJhdXRoX3RpbWUiOjAsInR5cCI6IiIsImF6cCI6IiIsInNlc3Npb25fc3RhdGUiOiIiLCJhY3IiOiIiLCJhbGxvd2VkLW9yaWdpbnMiOm51bGwsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6bnVsbH0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpudWxsfX0sInNjb3BlIjoiIiwic2lkIjoiIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiIiwiQ2xpZW50SUQiOiIiLCJjb21wYW55X2lkIjoiIiwiZ3JvdXBzIjpudWxsLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiIiLCJnaXZlbl9uYW1lIjoiIiwiZmFtaWx5X25hbWUiOiIiLCJlbWFpbCI6InRhemV0ZGlub3ZuQGdtYWlsLmNvbSJ9.xPxWUWA8e_GEUQ8bgpuHOZvoY7Xu2udRtGPrpHZrzP3Z5uhX-NqDvVDDZQRSpuqoAeXjmekTxX0kHFvwEmv-Kd1hnQZJA_nJf3DvSVPYErtu4ePgy4U2N-4uwlMvjtRysxv17SMEPaDP4XAKj6SxDZ8eEtRCnrTJpgxwh-2cz1NRUsH99pIBY0doj7yNkPOQnnrOCQpnzWdX-d1I1ERCwGg10qOa5VpxP3yXoc2PuDMF6gFU_1VxlhCEdL94TTlFQDPdaBmaDk6hDwUq6T7RZk4cRQ8eao10tUWLHucfaMBGtKEutgrbqR8kOmMB6wY3x86TXdZfBS0zr7RO137d-g ---------------------------------- ntazetdinov@nubes.ru