feat: переменные окружения функций (env vars в CRD)
- 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
This commit is contained in:
@@ -55,7 +55,7 @@ spec:
|
|||||||
serviceAccountName: fission-console
|
serviceAccountName: fission-console
|
||||||
containers:
|
containers:
|
||||||
- name: console
|
- name: console
|
||||||
image: naeel/fission-console:v1.3.71
|
image: naeel/fission-console:v1.3.72
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 8090
|
- containerPort: 8090
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
// Package api — CRUD операции с функциями: чтение, удаление, обновление таймаута, логи.
|
// Package api — CRUD операции с функциями: чтение, удаление, обновление таймаута, логи, env vars.
|
||||||
//
|
//
|
||||||
// Этот файл содержит операции, не связанные с заменой кода/архива:
|
// Этот файл содержит операции, не связанные с заменой кода/архива:
|
||||||
// - handleGetFunction — GET /functions/:name (детали: код, route, environment, source_type)
|
// - handleGetFunction — GET /functions/:name (детали: код, route, environment, source_type)
|
||||||
// - handleDeleteFunction — DELETE /functions/:name (каскадное удаление: триггеры, Package, S3)
|
// - handleDeleteFunction — DELETE /functions/:name (каскадное удаление: триггеры, Package, S3)
|
||||||
// - handleUpdateFunctionTimeout — PUT /functions/:name/timeout (только таймаут, без замены кода)
|
// - handleUpdateFunctionTimeout — PUT /functions/:name/timeout (только таймаут, без замены кода)
|
||||||
// - handleGetFunctionLogs — GET /functions/:name/logs (логи пода через Kubernetes API)
|
// - handleGetFunctionLogs — GET /functions/:name/logs (логи пода через Kubernetes API)
|
||||||
|
// - handleGetFunctionEnvVars — GET /functions/:name/envvars (переменные окружения из CRD)
|
||||||
|
// - handlePutFunctionEnvVars — PUT /functions/:name/envvars (обновить env vars в CRD)
|
||||||
//
|
//
|
||||||
// Операции с кодом и архивом — в function_code.go и function_archive.go соответственно.
|
// Операции с кодом и архивом — в function_code.go и function_archive.go соответственно.
|
||||||
// Вызов функции — в function_invoke.go.
|
// Вызов функции — в function_invoke.go.
|
||||||
@@ -102,6 +104,7 @@ func (s *Server) handleGetFunction(w http.ResponseWriter, r *http.Request, name
|
|||||||
"archive_filename": archiveFilename,
|
"archive_filename": archiveFilename,
|
||||||
"route": route,
|
"route": route,
|
||||||
"methods": methods,
|
"methods": methods,
|
||||||
|
"env_vars": extractEnvVars(fn),
|
||||||
"raw": fn.Object,
|
"raw": fn.Object,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -284,3 +287,109 @@ func (s *Server) handleGetFunctionLogs(w http.ResponseWriter, r *http.Request, n
|
|||||||
"logs": allLogs.String(),
|
"logs": allLogs.String(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleGetFunctionEnvVars возвращает переменные окружения функции из .spec.runtime.container.env
|
||||||
|
func (s *Server) handleGetFunctionEnvVars(w http.ResponseWriter, r *http.Request, name string) {
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
ns := s.userNS(r)
|
||||||
|
|
||||||
|
fn, err := s.dyn.Resource(fission.FunctionGVR).Namespace(ns).Get(ctx, name, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
status := http.StatusBadGateway
|
||||||
|
if apierrors.IsNotFound(err) {
|
||||||
|
status = http.StatusNotFound
|
||||||
|
}
|
||||||
|
writeJSONError(w, status, fmt.Sprintf("get function %q: %v", name, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
envVars := extractEnvVars(fn)
|
||||||
|
writeAnyJSON(w, http.StatusOK, map[string]any{"env_vars": envVars})
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePutFunctionEnvVars обновляет переменные окружения функции в .spec.runtime.container.env
|
||||||
|
func (s *Server) handlePutFunctionEnvVars(w http.ResponseWriter, r *http.Request, name string) {
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
ns := s.userNS(r)
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
EnvVars []map[string]string `json:"env_vars"` // [{name: "KEY", value: "VAL"}, ...]
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("decode request: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Валидация: имена переменных
|
||||||
|
for _, ev := range req.EnvVars {
|
||||||
|
k := ev["name"]
|
||||||
|
if k == "" {
|
||||||
|
writeJSONError(w, http.StatusBadRequest, "env var name cannot be empty")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn, err := s.dyn.Resource(fission.FunctionGVR).Namespace(ns).Get(ctx, name, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
status := http.StatusBadGateway
|
||||||
|
if apierrors.IsNotFound(err) {
|
||||||
|
status = http.StatusNotFound
|
||||||
|
}
|
||||||
|
writeJSONError(w, status, fmt.Sprintf("get function %q: %v", name, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Формируем список [{name: ..., value: ...}] для .spec.runtime.container.env
|
||||||
|
envList := make([]any, 0, len(req.EnvVars))
|
||||||
|
for _, ev := range req.EnvVars {
|
||||||
|
envList = append(envList, map[string]any{
|
||||||
|
"name": ev["name"],
|
||||||
|
"value": ev["value"],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем .spec.runtime.container.env
|
||||||
|
if err := unstructured.SetNestedSlice(fn.Object, envList, "spec", "runtime", "container", "env"); err != nil {
|
||||||
|
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("set env vars: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем аннотацию updated-at
|
||||||
|
now := time.Now().UTC().Format(time.RFC3339)
|
||||||
|
ann := fn.GetAnnotations()
|
||||||
|
if ann == nil {
|
||||||
|
ann = map[string]string{}
|
||||||
|
}
|
||||||
|
ann[functionUpdatedAtAnnotation] = now
|
||||||
|
fn.SetAnnotations(ann)
|
||||||
|
|
||||||
|
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)})
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractEnvVars читает .spec.runtime.container.env из Function CRD и возвращает [{name, value}, ...]
|
||||||
|
func extractEnvVars(fn *unstructured.Unstructured) []map[string]string {
|
||||||
|
raw, found, _ := unstructured.NestedSlice(fn.Object, "spec", "runtime", "container", "env")
|
||||||
|
if !found || len(raw) == 0 {
|
||||||
|
return []map[string]string{}
|
||||||
|
}
|
||||||
|
result := make([]map[string]string, 0, len(raw))
|
||||||
|
for _, item := range raw {
|
||||||
|
m, ok := item.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
k, _ := m["name"].(string)
|
||||||
|
v, _ := m["value"].(string)
|
||||||
|
if k != "" {
|
||||||
|
result = append(result, map[string]string{"name": k, "value": v})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|||||||
@@ -126,6 +126,19 @@ func (s *Server) handleFunctionsAction(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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)
|
http.NotFound(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+13
-2
@@ -102,7 +102,7 @@
|
|||||||
<div class="nubes">NUBES</div>
|
<div class="nubes">NUBES</div>
|
||||||
<div class="product">FISSION CONSOLE</div>
|
<div class="product">FISSION CONSOLE</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="font-size:0.65rem; color:var(--text-secondary); margin-left:10px; align-self:center; opacity:0.7;">v1.3.71</div>
|
<div style="font-size:0.65rem; color:var(--text-secondary); margin-left:10px; align-self:center; opacity:0.7;">v1.3.72</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row" style="margin:0;">
|
<div class="row" style="margin:0;">
|
||||||
<button class="btn ghost" onclick="reloadAll()">Refresh</button>
|
<button class="btn ghost" onclick="reloadAll()">Refresh</button>
|
||||||
@@ -389,6 +389,17 @@
|
|||||||
style="display:none;margin-top:8px;padding:10px 12px;border-radius:6px;font-size:13px;line-height:1.5;white-space:pre-wrap;font-family:monospace;">
|
style="display:none;margin-top:8px;padding:10px 12px;border-radius:6px;font-size:13px;line-height:1.5;white-space:pre-wrap;font-family:monospace;">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Переменные окружения -->
|
||||||
|
<div style="margin-top:16px;">
|
||||||
|
<div style="font-weight:600; margin-bottom:8px; font-size:13px;">Переменные окружения</div>
|
||||||
|
<div id="e-tf-env-warn" style="display:none; margin-bottom:8px; padding:6px 10px; background:#553300; color:#ffa; border-radius:4px; font-size:12px;">
|
||||||
|
⚠️ Функция управляется Terraform — переменные только для чтения
|
||||||
|
</div>
|
||||||
|
<div id="e-envvars-list" style="display:flex; flex-direction:column; gap:4px; margin-bottom:8px;"></div>
|
||||||
|
<button id="e-add-envvar-btn" class="btn ghost" style="font-size:12px;" onclick="addEnvVarRow('e')">+ Добавить переменную</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="btn ghost" onclick="closeEdit()">Отмена</button>
|
<button class="btn ghost" onclick="closeEdit()">Отмена</button>
|
||||||
<button id="e-submit" class="btn" onclick="submitEdit()">Сохранить</button>
|
<button id="e-submit" class="btn" onclick="submitEdit()">Сохранить</button>
|
||||||
@@ -498,7 +509,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions" style="justify-content:space-between; align-items:center;">
|
<div class="actions" style="justify-content:space-between; align-items:center;">
|
||||||
<span style="font-size:0.75rem; color:var(--text-secondary);">v1.3.71</span>
|
<span style="font-size:0.75rem; color:var(--text-secondary);">v1.3.72</span>
|
||||||
<button class="btn ghost" onclick="closeHelp()">Закрыть</button>
|
<button class="btn ghost" onclick="closeHelp()">Закрыть</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -191,6 +191,11 @@ async function openEdit(name) {
|
|||||||
var isTf = /^tf-/.test(name) || /go[-_]env/.test(fn.environment || '');
|
var isTf = /^tf-/.test(name) || /go[-_]env/.test(fn.environment || '');
|
||||||
warnEl.style.display = isTf ? 'block' : 'none';
|
warnEl.style.display = isTf ? 'block' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Env vars — отрисовать блок; TF-функции — только чтение
|
||||||
|
var isTfFn = /^tf-/.test(name) || /go[-_]env/.test(fn.environment || '');
|
||||||
|
renderEnvVars('e', fn.env_vars || [], isTfFn);
|
||||||
|
|
||||||
document.getElementById('edit-modal').classList.add('open');
|
document.getElementById('edit-modal').classList.add('open');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showStatus('Ошибка загрузки функции: ' + e.message, 'err');
|
showStatus('Ошибка загрузки функции: ' + e.message, 'err');
|
||||||
@@ -238,6 +243,16 @@ async function submitEdit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await syncScheduleForFunction(name, 'e');
|
await syncScheduleForFunction(name, 'e');
|
||||||
|
|
||||||
|
// Сохраняем env vars (только если не TF-функция)
|
||||||
|
var isTfFn = /^tf-/.test(name);
|
||||||
|
if (!isTfFn) {
|
||||||
|
var envVars = collectEnvVars('e');
|
||||||
|
await requestJSON(API_BASE + '/functions/' + encodeURIComponent(name) + '/envvars', 'PUT', {
|
||||||
|
env_vars: envVars
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
closeEdit();
|
closeEdit();
|
||||||
progress.stop('Код обновлён: ' + name, 'ok');
|
progress.stop('Код обновлён: ' + name, 'ok');
|
||||||
await reloadAll();
|
await reloadAll();
|
||||||
@@ -260,3 +275,91 @@ async function removeFn(name) {
|
|||||||
progress.stop('Ошибка удаления: ' + e.message, 'err');
|
progress.stop('Ошибка удаления: ' + e.message, 'err');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Env Vars UI ---
|
||||||
|
|
||||||
|
// renderEnvVars отрисовывает список переменных окружения в блоке prefix-envvars-list
|
||||||
|
// vars: [{name: "KEY", value: "VAL"}, ...]
|
||||||
|
// readOnly: true для TF-функций
|
||||||
|
function renderEnvVars(prefix, vars, readOnly) {
|
||||||
|
var list = document.getElementById(prefix + '-envvars-list');
|
||||||
|
var addBtn = document.getElementById(prefix + '-add-envvar-btn');
|
||||||
|
var tfWarn = document.getElementById(prefix + '-tf-env-warn');
|
||||||
|
if (!list) return;
|
||||||
|
list.innerHTML = '';
|
||||||
|
if (readOnly) {
|
||||||
|
if (tfWarn) tfWarn.style.display = '';
|
||||||
|
if (addBtn) addBtn.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
if (tfWarn) tfWarn.style.display = 'none';
|
||||||
|
if (addBtn) addBtn.style.display = '';
|
||||||
|
}
|
||||||
|
(vars || []).forEach(function(ev, idx) {
|
||||||
|
list.appendChild(makeEnvVarRow(prefix, ev.name || '', ev.value || '', readOnly, idx));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// makeEnvVarRow создаёт одну строку key=value с кнопкой удаления
|
||||||
|
function makeEnvVarRow(prefix, key, val, readOnly, idx) {
|
||||||
|
var row = document.createElement('div');
|
||||||
|
row.style.cssText = 'display:flex; gap:6px; align-items:center;';
|
||||||
|
row.dataset.envIdx = idx;
|
||||||
|
|
||||||
|
var kInput = document.createElement('input');
|
||||||
|
kInput.placeholder = 'KEY';
|
||||||
|
kInput.value = key;
|
||||||
|
kInput.disabled = readOnly;
|
||||||
|
kInput.style.cssText = 'flex:1; font-size:12px; font-family:monospace;';
|
||||||
|
kInput.dataset.envKey = '1';
|
||||||
|
|
||||||
|
var vInput = document.createElement('input');
|
||||||
|
vInput.placeholder = 'value';
|
||||||
|
vInput.value = val;
|
||||||
|
vInput.disabled = readOnly;
|
||||||
|
vInput.style.cssText = 'flex:2; font-size:12px; font-family:monospace;';
|
||||||
|
vInput.dataset.envVal = '1';
|
||||||
|
|
||||||
|
row.appendChild(kInput);
|
||||||
|
|
||||||
|
var eq = document.createElement('span');
|
||||||
|
eq.textContent = '=';
|
||||||
|
eq.style.cssText = 'color:var(--text-secondary); font-family:monospace;';
|
||||||
|
row.appendChild(eq);
|
||||||
|
row.appendChild(vInput);
|
||||||
|
|
||||||
|
if (!readOnly) {
|
||||||
|
var delBtn = document.createElement('button');
|
||||||
|
delBtn.textContent = '×';
|
||||||
|
delBtn.className = 'btn ghost';
|
||||||
|
delBtn.style.cssText = 'padding:2px 8px; font-size:14px; line-height:1;';
|
||||||
|
delBtn.title = 'Удалить переменную';
|
||||||
|
delBtn.onclick = function() { row.remove(); };
|
||||||
|
row.appendChild(delBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
// addEnvVarRow добавляет пустую строку в список env vars
|
||||||
|
function addEnvVarRow(prefix) {
|
||||||
|
var list = document.getElementById(prefix + '-envvars-list');
|
||||||
|
if (!list) return;
|
||||||
|
var idx = list.children.length;
|
||||||
|
list.appendChild(makeEnvVarRow(prefix, '', '', false, idx));
|
||||||
|
}
|
||||||
|
|
||||||
|
// collectEnvVars читает текущие значения из DOM и возвращает [{name, value}, ...]
|
||||||
|
function collectEnvVars(prefix) {
|
||||||
|
var list = document.getElementById(prefix + '-envvars-list');
|
||||||
|
if (!list) return [];
|
||||||
|
var result = [];
|
||||||
|
var rows = list.querySelectorAll('div[data-env-idx]');
|
||||||
|
rows.forEach(function(row) {
|
||||||
|
var k = row.querySelector('[data-env-key]');
|
||||||
|
var v = row.querySelector('[data-env-val]');
|
||||||
|
var key = k ? k.value.trim() : '';
|
||||||
|
var val = v ? v.value : '';
|
||||||
|
if (key) result.push({name: key, value: val});
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# Переменные окружения функций (env vars)
|
||||||
|
|
||||||
|
## Хранение
|
||||||
|
|
||||||
|
Env vars хранятся прямо в Function CRD в поле `.spec.runtime.container.env`:
|
||||||
|
|
||||||
|
```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"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Почему CRD, а не ConfigMap/PostgreSQL:**
|
||||||
|
- Env vars — часть конфигурации функции, а не статистика
|
||||||
|
- CRD хранится в etcd — единственном хранилище состояния Kubernetes
|
||||||
|
- При удалении функции env vars удаляются автоматически (нет orphan-объектов)
|
||||||
|
- Не нужен дополнительный RBAC для ConfigMap
|
||||||
|
- Fission executor сам передаёт `.spec.runtime.container.env` в pod
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### GET /console/api/functions/:name/envvars
|
||||||
|
Возвращает список переменных:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"env_vars": [
|
||||||
|
{"name": "DB_HOST", "value": "localhost"},
|
||||||
|
{"name": "API_KEY", "value": "secret123"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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 (разрешены любые непустые строки)
|
||||||
Reference in New Issue
Block a user