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