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