fix: env vars через newdeploy+podspec — попадают в pod OS env для всех языков
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user