fix: env vars через newdeploy+podspec — попадают в pod OS env для всех языков

This commit is contained in:
“Naeel”
2026-05-09 07:49:36 +04:00
parent 089994256c
commit 7887d4597d
4 changed files with 190 additions and 89 deletions
+1 -1
View File
@@ -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
+71 -4
View File
@@ -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
+2 -2
View File
@@ -102,7 +102,7 @@
<div class="nubes">NUBES</div>
<div class="product">FISSION CONSOLE</div>
</div>
<div style="font-size:0.65rem; color:var(--text-secondary); margin-left:10px; align-self:center; opacity:0.7;">v1.3.76</div>
<div style="font-size:0.65rem; color:var(--text-secondary); margin-left:10px; align-self:center; opacity:0.7;">v1.3.77</div>
</div>
<div class="row" style="margin:0;">
<button class="btn ghost" onclick="reloadAll()">Refresh</button>
@@ -510,7 +510,7 @@
</div>
<div class="actions" style="justify-content:space-between; align-items:center;">
<span style="font-size:0.75rem; color:var(--text-secondary);">v1.3.76</span>
<span style="font-size:0.75rem; color:var(--text-secondary);">v1.3.77</span>
<button class="btn ghost" onclick="closeHelp()">Закрыть</button>
</div>
</div>
+116 -82
View File
@@ -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>
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