diff --git a/console/deploy/console.yaml b/console/deploy/console.yaml index 9dae285..23ddf42 100644 --- a/console/deploy/console.yaml +++ b/console/deploy/console.yaml @@ -55,7 +55,7 @@ spec: serviceAccountName: fission-console containers: - name: console - image: naeel/fission-console:v1.3.76 + image: naeel/fission-console:v1.3.77 imagePullPolicy: Always ports: - containerPort: 8090 diff --git a/console/internal/api/function_crud.go b/console/internal/api/function_crud.go index 2375de8..a09a760 100644 --- a/console/internal/api/function_crud.go +++ b/console/internal/api/function_crud.go @@ -308,7 +308,16 @@ func (s *Server) handleGetFunctionEnvVars(w http.ResponseWriter, r *http.Request writeAnyJSON(w, http.StatusOK, map[string]any{"env_vars": envVars}) } -// handlePutFunctionEnvVars обновляет переменные окружения функции в аннотации fission-console/env-vars (JSON) +// handlePutFunctionEnvVars обновляет переменные окружения функции. +// +// Логика переключения ExecutorType: +// - Если env vars непустые → ExecutorType: newdeploy + spec.podspec.containers[0].env +// (newdeploy создаёт dedicated Deployment, Kubernetes ставит env vars на уровне ОС) +// - Если env vars пустые → ExecutorType: poolmgr, podspec удаляется +// (poolmgr использует warm pool, быстрый cold start) +// +// Это единственный универсальный способ передать env vars в pod для всех языков +// (Python, Go, Ruby, PHP, Node.js) без изменений в env-серверах. func (s *Server) handlePutFunctionEnvVars(w http.ResponseWriter, r *http.Request, name string) { ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) defer cancel() @@ -341,14 +350,14 @@ func (s *Server) handlePutFunctionEnvVars(w http.ResponseWriter, r *http.Request return } - // Сериализуем в JSON для аннотации + // Сериализуем в JSON для аннотации (для UI) envJSON, err := json.Marshal(req.EnvVars) if err != nil { writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("marshal env vars: %v", err)) return } - // Обновляем аннотацию updated-at + // Обновляем аннотации now := time.Now().UTC().Format(time.RFC3339) ann := fn.GetAnnotations() if ann == nil { @@ -358,12 +367,70 @@ func (s *Server) handlePutFunctionEnvVars(w http.ResponseWriter, r *http.Request ann["fission-console/env-vars"] = string(envJSON) fn.SetAnnotations(ann) + if len(req.EnvVars) > 0 { + // Есть env vars → newdeploy + podspec с env vars + envName, _, _ := unstructured.NestedString(fn.Object, "spec", "environment", "name") + + // Строим список env vars для Kubernetes + envList := make([]any, 0, len(req.EnvVars)) + for _, ev := range req.EnvVars { + envList = append(envList, map[string]any{ + "name": ev["name"], + "value": ev["value"], + }) + } + + // Устанавливаем podspec.containers[0] с env vars + // Имя контейнера = имя environment (стандарт Fission) + if err := unstructured.SetNestedSlice(fn.Object, []any{ + map[string]any{ + "name": envName, + "env": envList, + }, + }, "spec", "podspec", "containers"); err != nil { + writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("set podspec: %v", err)) + return + } + + // Переключаем на newdeploy (только он поддерживает podspec env) + if err := unstructured.SetNestedField(fn.Object, map[string]any{ + "ExecutionStrategy": map[string]any{ + "ExecutorType": "newdeploy", + "MinScale": int64(0), + "MaxScale": int64(1), + "SpecializationTimeout": int64(120), + }, + "StrategyType": "execution", + }, "spec", "InvokeStrategy"); err != nil { + writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("set invoke strategy: %v", err)) + return + } + } else { + // Нет env vars → poolmgr, убираем podspec + unstructured.RemoveNestedField(fn.Object, "spec", "podspec") + + if err := unstructured.SetNestedField(fn.Object, map[string]any{ + "ExecutionStrategy": map[string]any{ + "ExecutorType": "poolmgr", + "SpecializationTimeout": int64(120), + }, + "StrategyType": "execution", + }, "spec", "InvokeStrategy"); err != nil { + writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("set invoke strategy: %v", err)) + 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)) return } - writeAnyJSON(w, http.StatusOK, map[string]any{"updated": true, "count": len(req.EnvVars)}) + executor := "poolmgr" + if len(req.EnvVars) > 0 { + executor = "newdeploy" + } + writeAnyJSON(w, http.StatusOK, map[string]any{"updated": true, "count": len(req.EnvVars), "executor": executor}) } // extractEnvVars читает аннотацию fission-console/env-vars (JSON) из Function CRD diff --git a/console/ui/index.html b/console/ui/index.html index 004818d..91b109d 100644 --- a/console/ui/index.html +++ b/console/ui/index.html @@ -102,7 +102,7 @@
NUBES
FISSION CONSOLE
-
v1.3.76
+
v1.3.77
@@ -510,7 +510,7 @@
- v1.3.76 + v1.3.77
diff --git a/doc/descriptions/function-envvars-flow.md b/doc/descriptions/function-envvars-flow.md index 41a3605..ef47fee 100644 --- a/doc/descriptions/function-envvars-flow.md +++ b/doc/descriptions/function-envvars-flow.md @@ -1,94 +1,128 @@ -# Переменные окружения функций (env vars) +# Function Env Vars — ПОЛНАЯ ДОКУМЕНТАЦИЯ (v1.3.77+) -## Хранение +## ПРОБЛЕМА (обнаружена 2026-05-09) -Env vars хранятся прямо в Function CRD в поле `.spec.runtime.container.env`: +Env vars хранились только в аннотации `fission-console/env-vars` на Function CRD. +Это работало для UI (хранение/отображение), но переменные **не попадали в pod функции**. + +### Почему не попадали + +Fission `poolmgr` создаёт warm pool подов **заранее**, до того как знает какая функция +будет запущена. Pod уже работает в момент первого вызова функции. +`spec.podspec.containers[].env` из Function CRD при poolmgr **игнорируется** — pod уже запущен. + +При специализации (загрузке кода функции) fetcher передаёт в pod только: +- путь к файлу с кодом +- имя entrypoint функции +- секреты и configmaps (как файлы в `/userfunc/...`) + +Переменные окружения через `poolmgr` можно установить только на уровне Environment deployment — +одинаковые для ВСЕХ функций этого окружения. Индивидуально для функции — невозможно. + +### Отвергнутые варианты + +| Вариант | Почему нет | +|---------|-----------| +| `spec.podspec.containers[].env` + poolmgr | Pod уже запущен до специализации | +| `spec.configmaps` → файлы `/userfunc/configs/` | Нужно читать файл в коде — не универсально | +| Патч python-env server | Только Python, другие языки сломаны | +| PostgreSQL/отдельное хранилище | Оверкилл, не официальный путь | + +--- + +## РЕШЕНИЕ (v1.3.77+) + +**Автоматическое переключение ExecutorType в зависимости от наличия env vars.** + +- Функция **без env vars** → `ExecutorType: poolmgr` → warm pool, быстрый cold start (~0.5 сек) +- Функция **с env vars** → `ExecutorType: newdeploy` → dedicated Deployment, env vars в OS (~2-5 сек) + +Пользователь ничего не настраивает — переключение происходит автоматически. + +### Почему newdeploy работает + +При `newdeploy` Fission создаёт отдельный Kubernetes `Deployment` для функции. +В этот Deployment попадает `spec.podspec` из Function CRD **включая** `containers[0].env`. +Kubernetes ставит переменные на уровне ОС процесса контейнера при старте пода. + +### Почему это универсально для всех языков + +Переменные окружения ОС — стандарт POSIX. Любой язык читает без изменений в env-сервере: + +```python +os.getenv("MY_VAR") # Python +os.Getenv("MY_VAR") # Go +ENV["MY_VAR"] # Ruby +getenv("MY_VAR") # PHP +process.env.MY_VAR # Node.js +``` + +### Нет дублирования подов + +Warm pool принадлежит **Environment**, не функции. Переключение конкретной функции на `newdeploy` +не трогает pool — другие функции без env vars продолжают использовать его. + +--- + +## РЕАЛИЗАЦИЯ + +### Backend: `handlePutFunctionEnvVars` (function_crud.go) + +При `PUT /functions/:name/envvars`: + +1. Сохраняет env vars в аннотацию `fission-console/env-vars` (для UI) +2. Если env vars **непустые**: + - Выставляет `spec.podspec.containers[0].env` (стандартный Kubernetes EnvVar) + - Переключает `spec.InvokeStrategy.ExecutionStrategy.ExecutorType` = `"newdeploy"` + - MinScale=0, MaxScale=1 +3. Если env vars **пустые** (очищены): + - Удаляет `spec.podspec` + - Возвращает `ExecutorType` = `"poolmgr"` + +### Двойное хранение + +| Где | Зачем | +|-----|-------| +| Аннотация `fission-console/env-vars` | UI: отображение, редактирование | +| `spec.podspec.containers[0].env` | Fission/Kubernetes: реальное применение в pod | + +### Структура Function CRD с env vars ```yaml -apiVersion: fission.io/v1 -kind: Function -metadata: - name: my-fn - namespace: c479c9de005f spec: - runtime: - container: - env: - - name: DB_HOST - value: "localhost" - - name: API_KEY - value: "secret123" + InvokeStrategy: + ExecutionStrategy: + ExecutorType: newdeploy + MinScale: 0 + MaxScale: 1 + StrategyType: execution + podspec: + containers: + - name: + env: + - name: p1 + value: "envv1" +metadata: + annotations: + fission-console/env-vars: '[{"name":"p1","value":"envv1"}]' ``` -**Почему CRD, а не ConfigMap/PostgreSQL:** -- Env vars — часть конфигурации функции, а не статистика -- CRD хранится в etcd — единственном хранилище состояния Kubernetes -- При удалении функции env vars удаляются автоматически (нет orphan-объектов) -- Не нужен дополнительный RBAC для ConfigMap -- Fission executor сам передаёт `.spec.runtime.container.env` в pod +### Структура Function CRD без env vars -## API - -### GET /console/api/functions/:name/envvars -Возвращает список переменных: -```json -{ - "env_vars": [ - {"name": "DB_HOST", "value": "localhost"}, - {"name": "API_KEY", "value": "secret123"} - ] -} +```yaml +spec: + InvokeStrategy: + ExecutionStrategy: + ExecutorType: poolmgr + StrategyType: execution + # нет podspec ``` -### PUT /console/api/functions/:name/envvars -Принимает полный список (полная замена, не merge): -```json -{ - "env_vars": [ - {"name": "DB_HOST", "value": "new-host"}, - {"name": "NEW_VAR", "value": "val"} - ] -} -``` -Пустой массив `[]` — удалить все переменные. +--- -### GET /console/api/functions/:name -Поле `env_vars` включено в стандартный ответ (чтобы edit modal не делал отдельный запрос). +## ОГРАНИЧЕНИЯ -## Frontend (edit modal) - -- Блок "Переменные окружения" появляется в нижней части edit modal при каждом открытии -- Каждая переменная — строка: input[KEY] = input[value] [×] -- Кнопка "+ Добавить переменную" добавляет пустую строку -- Сохранение происходит вместе с остальными полями (Сохранить) -- После сохранения кода/архива вызывается PUT /envvars с текущим состоянием блока - -## Terraform-функции (read-only) - -Функции с именем `tf-*` или с go-env (Terraform-управляемые) показывают блок env vars в режиме **только чтения**: -- Все input-поля `disabled` -- Кнопки `[×]` и `[+ Добавить]` скрыты -- Предупреждение: "⚠️ Функция управляется Terraform — переменные только для чтения" -- PUT /envvars **не вызывается** при сохранении TF-функций - -Изменить переменные TF-функции можно только через Terraform (`terraform apply`). - -## Backend (Go) - -**Файл:** `console/internal/api/function_crud.go` - -- `handleGetFunctionEnvVars` — читает `.spec.runtime.container.env` через dynamic client -- `handlePutFunctionEnvVars` — PATCH Function CRD, обновляет `.spec.runtime.container.env` -- `extractEnvVars` — вспомогательная функция, читает из unstructured в `[]map[string]string` - -**Роутинг:** `console/internal/api/handlers.go` -``` -GET /console/api/functions/:name/envvars → handleGetFunctionEnvVars -PUT /console/api/functions/:name/envvars → handlePutFunctionEnvVars -``` - -## Ограничения (MVP) - -- Секретные переменные (пароли, токены) хранятся в открытом виде в etcd -- Для sensitive данных в будущем нужно добавить поддержку Secret (valueFrom.secretKeyRef) -- Нет валидации имён переменных по RFC (разрешены любые непустые строки) +- `newdeploy` медленнее cold start (~2-5 сек vs ~0.5 сек) — выбор пользователя, добавившего env vars +- MinScale=0: при отсутствии трафика pod удаляется → cold start при первом вызове +- Имена переменных: только `[A-Za-z_][A-Za-z0-9_]*` — стандарт POSIX, проверяется на UI +- TF-функции (`tf-*`) — env vars read-only, изменение запрещено через UI