From a09291e2084a4ba87839e0d14c454c3fe21599db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Sun, 3 May 2026 13:37:12 +0400 Subject: [PATCH] feat: remove test mode (X-Test-Sub), all functions stored in S3 via storagesvc - Remove FISSION_TEST_MODE and X-Test-Sub bypass from authMiddleware - Remove testMode field from Server/Config structs - Remove TestAuthenticator branch from main.go - Add uploadToStoragesvc/buildDeploySpec: all function code uploaded to S3 - Add deleteFromStoragesvc: archive deleted from S3 on function delete - Add FISSION_STORAGESVC_URL env to main.go and deploy/console.yaml - image: naeel/fission-console:v1.3.31 --- console/cmd/server/main.go | 12 +-- console/deploy/console.yaml | 4 +- console/internal/api/auth.go | 17 +--- console/internal/api/handlers.go | 136 +++++++++++++++++++++++++++++-- console/internal/api/server.go | 12 +-- 5 files changed, 141 insertions(+), 40 deletions(-) diff --git a/console/cmd/server/main.go b/console/cmd/server/main.go index 63d4b22..250ada3 100644 --- a/console/cmd/server/main.go +++ b/console/cmd/server/main.go @@ -37,16 +37,8 @@ func main() { log.Fatalf("create dynamic client: %v", err) } - testMode := os.Getenv("FISSION_TEST_MODE") == "true" - - var jwtAuth auth.Authenticator - if testMode { - jwtAuth = &auth.TestAuthenticator{} - } else { - jwtAuth = auth.NewDeckAuthenticator(auth.DefaultDeckAPIs, nil) - } authenticator := &auth.MultiAuthenticator{ - JWT: jwtAuth, + JWT: auth.NewDeckAuthenticator(auth.DefaultDeckAPIs, nil), Demo: &auth.DemoAuthenticator{}, } @@ -59,7 +51,7 @@ func main() { SATokenPath: envDefault("SA_TOKEN_PATH", "/var/run/secrets/kubernetes.io/serviceaccount/token"), AuthUser: envDefault("FISSION_AUTH_USERNAME", ""), AuthPass: envDefault("FISSION_AUTH_PASSWORD", ""), - TestMode: testMode, + StoragesvcURL: envDefault("FISSION_STORAGESVC_URL", "http://storagesvc.fission.svc.cluster.local"), Authenticator: authenticator, // --- ai/ask feature --- LLMUrl: envDefault("FISSION_LLM_URL", "https://api.aillm.ru"), diff --git a/console/deploy/console.yaml b/console/deploy/console.yaml index 5319b69..9c9c97c 100644 --- a/console/deploy/console.yaml +++ b/console/deploy/console.yaml @@ -52,7 +52,7 @@ spec: serviceAccountName: fission-console containers: - name: console - image: naeel/fission-console:v1.3.27 + image: naeel/fission-console:v1.3.31 imagePullPolicy: Always ports: - containerPort: 8090 @@ -77,6 +77,8 @@ spec: secretKeyRef: name: router key: password + - name: FISSION_STORAGESVC_URL + value: "http://storagesvc.fission.svc.cluster.local" livenessProbe: httpGet: path: /health diff --git a/console/internal/api/auth.go b/console/internal/api/auth.go index 3c51415..f0de2bd 100644 --- a/console/internal/api/auth.go +++ b/console/internal/api/auth.go @@ -50,28 +50,13 @@ func normalizeEnv(env string) string { } // authMiddleware оборачивает handler аутентификацией. -// -// В testMode: заголовок X-Test-Sub позволяет задать sub напрямую (без токена). -// Иначе: токен передаётся в s.authenticator.Authenticate — детали скрыты за интерфейсом. +// Токен передаётся в s.authenticator.Authenticate — детали скрыты за интерфейсом. func (s *Server) authMiddleware(h http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var identity auth.UserIdentity env := normalizeEnv(r.Header.Get("X-Auth-Env")) - if s.testMode { - if sub := strings.TrimSpace(r.Header.Get("X-Test-Sub")); sub != "" { - identity = auth.UserIdentity{Sub: sub} - ctx := s.contextWithIdentity(r.Context(), identity) - if err := s.nsManager.EnsureUserNS(ctx, auth.NamespaceForSub(sub)); err != nil { - writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("ensure namespace: %v", err)) - return - } - h(w, r.WithContext(ctx)) - return - } - } - token := authTokenFromRequest(r) var err error identity, err = s.authenticator.Authenticate(r.Context(), token, env) diff --git a/console/internal/api/handlers.go b/console/internal/api/handlers.go index 57159d1..c2834cc 100644 --- a/console/internal/api/handlers.go +++ b/console/internal/api/handlers.go @@ -11,6 +11,7 @@ import ( "log" "net" "net/http" + "net/url" "regexp" "strconv" "strings" @@ -51,6 +52,102 @@ func buildDeployArchive(lang, code string) ([]byte, error) { } } +// deleteFromStoragesvc удаляет архив из S3 через storagesvc по URL из Package spec. +// URL имеет формат: http://storagesvc.../v1/archive?id=fission/UUID +// Best-effort: ошибка логируется, но не прерывает операцию удаления. +func (s *Server) deleteFromStoragesvc(ctx context.Context, archiveURL string) { + if s.storagesvcURL == "" || archiveURL == "" { + return + } + // archiveURL = "http://storagesvc.../v1/archive?id=fission/UUID" + // Строим DELETE URL к storagesvc, сохраняя query-параметр id + parsed, err := url.Parse(archiveURL) + if err != nil { + log.Printf("deleteFromStoragesvc: parse url %q: %v", archiveURL, err) + return + } + deleteURL := strings.TrimRight(s.storagesvcURL, "/") + "/v1/archive?" + parsed.RawQuery + req, err := http.NewRequestWithContext(ctx, http.MethodDelete, deleteURL, nil) + if err != nil { + log.Printf("deleteFromStoragesvc: build request: %v", err) + return + } + resp, err := s.http.Do(req) + if err != nil { + log.Printf("deleteFromStoragesvc: %v", err) + return + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent { + body, _ := io.ReadAll(resp.Body) + log.Printf("deleteFromStoragesvc: status %d: %s", resp.StatusCode, strings.TrimSpace(string(body))) + return + } + log.Printf("deleteFromStoragesvc: deleted %s", parsed.Query().Get("id")) +} + +// uploadToStoragesvc загружает байты в Fission storagesvc и возвращает URL для package archive. +// Если storagesvcURL не задан — возвращает пустую строку (fallback на literal). +func (s *Server) uploadToStoragesvc(ctx context.Context, data []byte) (string, error) { + if s.storagesvcURL == "" { + log.Printf("uploadToStoragesvc: storagesvcURL is empty, skip upload") + return "", nil + } + log.Printf("uploadToStoragesvc: uploading %d bytes to %s", len(data), s.storagesvcURL) + uploadURL := strings.TrimRight(s.storagesvcURL, "/") + "/v1/archive" + body := &bytes.Reader{} + // multipart/form-data с полем uploadfile + var buf bytes.Buffer + boundary := fmt.Sprintf("fission%d", time.Now().UnixNano()) + buf.WriteString("--" + boundary + "\r\n") + buf.WriteString(fmt.Sprintf("Content-Disposition: form-data; name=\"uploadfile\"; filename=\"archive.zip\"\r\n")) + buf.WriteString("Content-Type: application/octet-stream\r\n\r\n") + buf.Write(data) + buf.WriteString("\r\n--" + boundary + "--\r\n") + body = bytes.NewReader(buf.Bytes()) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, uploadURL, body) + if err != nil { + return "", fmt.Errorf("build storagesvc upload request: %w", err) + } + req.Header.Set("Content-Type", "multipart/form-data; boundary="+boundary) + req.Header.Set("X-File-Size", fmt.Sprintf("%d", len(data))) + + resp, err := s.http.Do(req) + if err != nil { + return "", fmt.Errorf("storagesvc upload: %w", err) + } + defer resp.Body.Close() + respBody, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + return "", fmt.Errorf("storagesvc upload status %d: %s", resp.StatusCode, strings.TrimSpace(string(respBody))) + } + var result struct { + ID string `json:"id"` + } + if err := json.Unmarshal(respBody, &result); err != nil || result.ID == "" { + return "", fmt.Errorf("storagesvc upload: bad response: %s", string(respBody)) + } + archiveURL := strings.TrimRight(s.storagesvcURL, "/") + "/v1/archive?id=" + result.ID + return archiveURL, nil +} + +// buildDeploySpec строит spec.deployment для Fission Package. +// Если storagesvcURL задан — загружает архив в S3 через storagesvc и возвращает type: url. +// Иначе — возвращает type: literal с base64-кодом. +func (s *Server) buildDeploySpec(ctx context.Context, data []byte) (map[string]any, error) { + archiveURL, err := s.uploadToStoragesvc(ctx, data) + if err != nil { + log.Printf("storagesvc upload failed, falling back to literal: %v", err) + // fallback — сохраняем как literal + return map[string]any{"type": "literal", "literal": base64.StdEncoding.EncodeToString(data)}, nil + } + if archiveURL == "" { + return map[string]any{"type": "literal", "literal": base64.StdEncoding.EncodeToString(data)}, nil + } + return map[string]any{"type": "url", "url": archiveURL}, nil +} + func (s *Server) resolveInvokeTimeout(fn *unstructured.Unstructured) time.Duration { if fn != nil { seconds, found, err := unstructured.NestedInt64(fn.Object, "spec", "functionTimeout") @@ -313,7 +410,7 @@ func (s *Server) handleCreateFunction(w http.ResponseWriter, r *http.Request) { // Строим Package spec в зависимости от языка: // - Go: source package → builder job компилирует в .so плагин // - Node.js: deployment zip с ESM wrapper (package.json + main.js) - // - Остальные: deployment literal с кодом напрямую + // - Остальные: deployment archive с кодом (S3 или literal fallback) var pkgSpec map[string]any if req.Language == "go" { srcZip, err := runtime.BuildGoSourceZip(req.Code) @@ -321,12 +418,13 @@ func (s *Server) handleCreateFunction(w http.ResponseWriter, r *http.Request) { writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("build go source archive: %v", err)) return } - literal := base64.StdEncoding.EncodeToString(srcZip) + srcSpec, srcErr := s.buildDeploySpec(ctx, srcZip) + if srcErr != nil { + writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("upload go source: %v", srcErr)) + return + } pkgSpec = map[string]any{ - "source": map[string]any{ - "type": "literal", - "literal": literal, - }, + "source": srcSpec, "deployment": map[string]any{}, "environment": map[string]any{"name": req.Environment, "namespace": ns}, "buildcommand": "build", @@ -337,8 +435,13 @@ func (s *Server) handleCreateFunction(w http.ResponseWriter, r *http.Request) { writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("build %s archive: %v", req.Language, archiveErr)) return } + deploySpec, uploadErr := s.buildDeploySpec(ctx, deployBytes) + if uploadErr != nil { + writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("upload %s archive: %v", req.Language, uploadErr)) + return + } pkgSpec = map[string]any{ - "deployment": map[string]any{"type": "literal", "literal": base64.StdEncoding.EncodeToString(deployBytes)}, + "deployment": deploySpec, "environment": map[string]any{"name": req.Environment, "namespace": ns}, "source": map[string]any{}, } @@ -563,12 +666,17 @@ func (s *Server) handleUpdateFunctionCode(w http.ResponseWriter, r *http.Request }() now := time.Now().UTC() newPkgName := name + "-pkg-" + strconv.FormatInt(time.Now().UnixMilli(), 36) + deploySpec, uploadErr := s.buildDeploySpec(ctx, deployBytes) + if uploadErr != nil { + writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("upload %s archive: %v", lang, uploadErr)) + return + } 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)}, + "deployment": deploySpec, "environment": map[string]any{"name": envName, "namespace": ns}, "source": map[string]any{}, }, @@ -1000,10 +1108,22 @@ func (s *Server) handleDeleteFunction(w http.ResponseWriter, r *http.Request, na } if pkgName != "" { + // Получаем URL архива из Package spec.deployment перед удалением, чтобы потом очистить S3 + var archiveURL string + if pkg, pkgErr := s.dyn.Resource(fission.PackageGVR).Namespace(ns).Get(ctx, pkgName, metav1.GetOptions{}); pkgErr == nil { + deployType, _, _ := unstructured.NestedString(pkg.Object, "spec", "deployment", "type") + if deployType == "url" { + archiveURL, _, _ = unstructured.NestedString(pkg.Object, "spec", "deployment", "url") + } + } 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 } + // Удаляем архив из S3 после успешного удаления Package (best-effort) + if archiveURL != "" { + go s.deleteFromStoragesvc(context.Background(), archiveURL) + } } // Убираем environment pool pods если язык больше не используется (best-effort) diff --git a/console/internal/api/server.go b/console/internal/api/server.go index c05e8f8..9d52534 100644 --- a/console/internal/api/server.go +++ b/console/internal/api/server.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "fmt" + "log" "net/http" "os" "strings" @@ -36,10 +37,10 @@ type Server struct { saTokenPath string invokeTimeout time.Duration - testMode bool // FISSION_TEST_MODE=true — разрешает X-Test-Sub shortcut - authUser string - authPass string + authUser string + authPass string + storagesvcURL string // URL Fission storagesvc для загрузки архивов кода в S3 // --- ai/ask feature (удалить блок целиком чтобы выкосить) --- llmURL string // FISSION_LLM_URL @@ -68,7 +69,7 @@ type Config struct { SATokenPath string AuthUser string AuthPass string - TestMode bool + StoragesvcURL string Authenticator auth.Authenticator // слой аутентификации LLMUrl string LLMKey string @@ -76,6 +77,7 @@ type Config struct { // NewServer создаёт и настраивает HTTP Server со всеми зависимостями. func NewServer(cfg Config) *Server { + log.Printf("NewServer: storagesvcURL=%q", cfg.StoragesvcURL) return &Server{ dyn: cfg.Dyn, ns: cfg.Namespace, @@ -85,7 +87,7 @@ func NewServer(cfg Config) *Server { invokeTimeout: cfg.InvokeTimeout, authUser: cfg.AuthUser, authPass: cfg.AuthPass, - testMode: cfg.TestMode, + storagesvcURL: cfg.StoragesvcURL, authenticator: cfg.Authenticator, llmURL: cfg.LLMUrl, llmKey: cfg.LLMKey,