- Backend: handleGetFunctionEnvVars / handlePutFunctionEnvVars в function_crud.go - API: GET/PUT /functions/:name/envvars - handleGetFunction теперь возвращает env_vars в ответе - Frontend: блок env vars в edit modal (key=value + [×] + [+ Добавить]) - TF-функции показывают env vars в режиме read-only - Документация: doc/descriptions/function-envvars-flow.md - Версия v1.3.72
280 lines
10 KiB
Go
280 lines
10 KiB
Go
// Package api — роутинг и общие утилиты HTTP-обработчиков консоли.
|
||
//
|
||
// Этот файл содержит:
|
||
// - Константы и regex для валидации функций
|
||
// - handleFunctionsRoot / handleFunctionsAction — диспетчеры запросов к /functions
|
||
// - handleAuth — аутентификация пользователя (POST /auth)
|
||
// - parseTTL, normalizeMethods, normalizeRoute и другие утилиты
|
||
//
|
||
// Реализация конкретных операций с функциями вынесена в отдельные файлы:
|
||
// - function_code.go — создание и обновление из кода
|
||
// - function_archive.go — создание и обновление из zip-архива
|
||
// - function_crud.go — GET, DELETE, обновление таймаута, логи
|
||
// - function_invoke.go — вызов функции через Fission router
|
||
// - storagesvc.go — загрузка/удаление архивов в S3 через storagesvc
|
||
package api
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"net/http"
|
||
"regexp"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"fission-console/internal/auth"
|
||
)
|
||
|
||
// validFuncName — RFC 1123 subdomain label: строчные буквы+цифры+дефис, без дефиса в начале/конце.
|
||
// Максимум 57 символов (не 63): самый длинный суффикс "-route" (HTTPTrigger) = 6 символов.
|
||
// 63 - 6 = 57. Fission webhook требует все связанные объекты <= 63 символов.
|
||
var validFuncName = regexp.MustCompile(`^[a-z0-9]([a-z0-9-]*[a-z0-9])?$`)
|
||
|
||
// ⛔⛔⛔ НЕ МЕНЯТЬ БЕЗ ЯВНОГО РАЗРЕШЕНИЯ ВЛАДЕЛЬЦА.
|
||
// Любые "улучшения", рефакторинг, новые фичи — ЗАПРЕЩЕНЫ без команды.
|
||
// Прецедент: самодеятельный helm upgrade сломал JWT secret router → 401 у всех функций.
|
||
|
||
// maxCodeSize — максимальный размер кода функции (1 MB).
|
||
// Выше — не имеет смысла для inline функции; лучше использовать Package с URL.
|
||
const maxCodeSize = 1 << 20
|
||
|
||
// maxArchiveUploadSize — максимальный размер zip-архива при загрузке функции (32 MB).
|
||
const maxArchiveUploadSize = 32 << 20
|
||
|
||
// fissionSourceTypeAnnotation — аннотация на Function, хранит тип источника: "code" или "archive".
|
||
const fissionSourceTypeAnnotation = "fission-console/source-type"
|
||
|
||
// defaultFunctionInvokeTimeout совпадает с дефолтом Fission для spec.functionTimeout.
|
||
const defaultFunctionInvokeTimeout = 60 * time.Second
|
||
|
||
// handleFunctionsRoot обрабатывает запросы к /console/api/functions без имени функции.
|
||
// GET → список всех функций со статусами, POST → создать новую.
|
||
func (s *Server) handleFunctionsRoot(w http.ResponseWriter, r *http.Request) {
|
||
switch r.Method {
|
||
case http.MethodGet:
|
||
s.handleListFunctionsWithStatus(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", "archive", "invoke", "logs", "timeout").
|
||
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/timeout — обновление только таймаута (без замены архива/кода)
|
||
if len(parts) == 2 && parts[1] == "timeout" && r.Method == http.MethodPut {
|
||
s.handleUpdateFunctionTimeout(w, r, name)
|
||
return
|
||
}
|
||
|
||
// /functions/:name/archive — обновление через zip-архив
|
||
if len(parts) == 2 && parts[1] == "archive" && r.Method == http.MethodPut {
|
||
s.handleUpdateFunctionArchive(w, r, name)
|
||
return
|
||
}
|
||
|
||
// /functions/:name/invoke — вызов функции
|
||
if len(parts) == 2 && parts[1] == "invoke" && r.Method == http.MethodPost {
|
||
s.handleInvokeFunction(w, r, name)
|
||
return
|
||
}
|
||
|
||
// /functions/:name/logs — логи подов функции
|
||
if len(parts) == 2 && parts[1] == "logs" && r.Method == http.MethodGet {
|
||
s.handleGetFunctionLogs(w, r, name)
|
||
return
|
||
}
|
||
|
||
// /functions/:name/envvars — переменные окружения
|
||
if len(parts) == 2 && parts[1] == "envvars" {
|
||
switch r.Method {
|
||
case http.MethodGet:
|
||
s.handleGetFunctionEnvVars(w, r, name)
|
||
case http.MethodPut:
|
||
s.handlePutFunctionEnvVars(w, r, name)
|
||
default:
|
||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||
}
|
||
return
|
||
}
|
||
|
||
http.NotFound(w, r)
|
||
}
|
||
|
||
// handleAuth обрабатывает POST /console/api/auth.
|
||
// Валидирует токен через authenticator, создаёт namespace, возвращает namespace + email.
|
||
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 := normalizeEnv(body.Env)
|
||
|
||
identity, err := s.authenticator.Authenticate(r.Context(), body.Token, env)
|
||
if err != nil {
|
||
writeJSONError(w, http.StatusUnauthorized, "invalid token")
|
||
return
|
||
}
|
||
|
||
ns := auth.NamespaceForSub(identity.Sub)
|
||
|
||
ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second)
|
||
defer cancel()
|
||
if ensureErr := s.nsManager.EnsureUserNS(ctx, ns); ensureErr != nil {
|
||
fmt.Printf("handleAuth: ensureUserNS %s: %v\n", 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, "email": identity.Email})
|
||
}
|
||
|
||
// parseTTL парсит строку TTL и возвращает время истечения.
|
||
// Поддерживаемые форматы: Go duration (1h, 30m, 24h) и дни (1d, 7d, 30d).
|
||
// Суффикс "d" не поддерживается стандартным time.ParseDuration — обрабатываем отдельно.
|
||
func parseTTL(ttl string) (time.Time, error) {
|
||
if strings.HasSuffix(ttl, "d") {
|
||
days, err := strconv.Atoi(strings.TrimSuffix(ttl, "d"))
|
||
if err != nil || days <= 0 {
|
||
return time.Time{}, fmt.Errorf("invalid days value: %q", ttl)
|
||
}
|
||
return time.Now().Add(time.Duration(days) * 24 * time.Hour), nil
|
||
}
|
||
d, err := time.ParseDuration(ttl)
|
||
if err != nil {
|
||
return time.Time{}, err
|
||
}
|
||
if d <= 0 {
|
||
return time.Time{}, fmt.Errorf("ttl must be positive")
|
||
}
|
||
return time.Now().Add(d), nil
|
||
}
|
||
|
||
// 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
|
||
}
|
||
|
||
// normalizeFunctionTimeout нормализует таймаут: отрицательные и нулевые значения
|
||
// заменяются дефолтным значением Fission (defaultFunctionInvokeTimeout).
|
||
func normalizeFunctionTimeout(seconds int64) int64 {
|
||
if seconds <= 0 {
|
||
return int64(defaultFunctionInvokeTimeout / time.Second)
|
||
}
|
||
return seconds
|
||
}
|
||
|
||
// normalizeRoute нормализует URL-маршрут: убирает пробелы, гарантирует prefix "/".
|
||
func normalizeRoute(route string) string {
|
||
route = strings.TrimSpace(route)
|
||
if route == "" || route == "/" {
|
||
return "/"
|
||
}
|
||
if !strings.HasPrefix(route, "/") {
|
||
return "/" + route
|
||
}
|
||
return route
|
||
}
|
||
|
||
// routeAllowsMethod проверяет, допускает ли маршрут указанный HTTP-метод.
|
||
// Пустой список methods означает "любой метод".
|
||
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
|
||
}
|
||
|
||
// appendUniqueMethods добавляет методы из src в dst без дублей (case-insensitive).
|
||
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
|
||
}
|