fix: restore NSReconciler, archive upload create/edit, fix version label both places; v1.3.45

- tenant.go: restore StartNSReconciler (sync FISSION_RESOURCE_NAMESPACES every 30s)
- main.go: call nsm.StartNSReconciler on startup
- handlers.go: handleCreateFunctionFromArchive (multipart), handleUpdateFunctionArchive (PUT /archive)
  source-type annotation, source_type in GET response
- functions.js: submitCreate/submitEdit archive mode, openEdit uses source_type
- index.html: version label updated in BOTH places (line 103 and 403) to v1.3.45
- console.yaml: image v1.3.45
- doc: archive-and-edit-modal-flow.md
This commit is contained in:
“Naeel”
2026-05-04 06:16:11 +04:00
parent db3815d03d
commit d342b649a8
8 changed files with 784 additions and 19 deletions
@@ -0,0 +1,239 @@
# Archive Upload и Edit Modal — описание логики (v1.3.45, 2026-05-04)
## Контекст
До v1.3.45 функции создавались только через ввод кода (JSON поле `code`).
Edit modal при открытии не сбрасывал режим (code/archive) — показывал то что было
выбрано ранее. Создание и обновление через zip-архив не поддерживалось вовсе.
---
## 1. Аннотация `fission-console/source-type`
Добавлена новая аннотация на Kubernetes-объекте `Function`:
```
fission-console/source-type: "code" | "archive"
```
Ставится в момент создания функции и обновляется при каждом сохранении.
Хранится в `metadata.annotations` Function объекта в K8s — персистентно.
**Зачем:** позволяет при открытии edit modal знать каким способом была создана
функция, без анализа содержимого Package (которое не даёт надёжного ответа).
---
## 2. Backend — создание функции
### 2a. Определение режима (handlers.go `handleCreateFunction`)
```go
isArchiveUpload := strings.HasPrefix(r.Header.Get("Content-Type"), "multipart/form-data")
if isArchiveUpload {
s.handleCreateFunctionFromArchive(w, r, ns)
return
}
```
Если Content-Type начинается с `multipart/form-data` — передаём в новый хендлер.
Иначе — старый JSON-путь без изменений.
### 2b. Создание через JSON-код (без изменений, кроме аннотации)
В `fnAnnotations` добавлено:
```go
fissionSourceTypeAnnotation: "code",
```
Все старые функции (созданные до v1.3.45) не имеют этой аннотации — API вернёт
`source_type: "code"` по умолчанию.
### 2c. Создание через архив (`handleCreateFunctionFromArchive`)
**Вход:** `multipart/form-data` с полями:
- `name`, `language`, `environment`, `entrypoint`, `route`, `methods`, `timeout`, `ttl`
- `archive` — файл `.zip`
**Алгоритм:**
1. `r.ParseMultipartForm(32MB)` — парсим форму
2. Валидация `name` по regexp `^[a-z0-9]([a-z0-9-]*[a-z0-9])?$`, max 57 символов
3. Если задан `language` — через `fission.EnsureEnvironment` создаём/получаем Environment
4. `r.FormFile("archive")` → `io.ReadAll(io.LimitReader(f, 32MB))` — читаем архив
5. `s.buildDeploySpec(ctx, archiveBytes)` — загружаем в Fission storagesvc (S3), получаем
`{type: "url", url: "http://storagesvc/.../archive?id=..."}` или fallback на literal
6. Создаём Package с `spec.deployment = deploySpec`
7. Создаём Function с аннотацией `fission-console/source-type: archive`
8. Создаём HTTPTrigger
9. При ошибке на шаге 7 или 8 — откат (удаляем уже созданные объекты)
**Ответ:** `201 Created` с JSON `{name, namespace, environment, route, source_type: "archive"}`
---
## 3. Backend — обновление функции через архив
### Новый endpoint: `PUT /console/api/functions/:name/archive`
Зарегистрирован в `handleFunctionsAction`:
```go
if len(parts) == 2 && parts[1] == "archive" && r.Method == http.MethodPut {
s.handleUpdateFunctionArchive(w, r, name)
return
}
```
### `handleUpdateFunctionArchive` — алгоритм:
1. Парсим multipart form (32MB лимит)
2. `r.FormFile("archive")` → читаем байты (лимит 32MB)
3. Получаем текущую Function из K8s
4. Запоминаем `oldPkgName` (для удаления после замены)
5. Генерируем `newPkgName = name + "-pkg-" + base36(unixMs)` — уникальное имя
6. Загружаем архив в storagesvc → создаём новый Package
7. Обновляем Function: `spec.package.packageref.name = newPkgName`
8. Обновляем аннотации: `source-type: archive`, `updated-at: now`
9. Обновляем `spec.functionTimeout` если передан `timeout`
10. Update Function в K8s
11. Удаляем старый Package (best-effort, ошибки игнорируем)
**Почему создаём новый Package вместо обновления старого:**
Executor кэширует function service по `functionUid`. Изменение содержимого
существующего Package не сбрасывает кэш. Новое имя пакета гарантирует cache miss.
(Та же логика что в `handleUpdateFunctionCode`.)
---
## 4. Backend — GET /functions/:name
Добавлено поле `source_type` в ответ:
```go
sourceType := "code" // default для старых функций без аннотации
if ann := fn.GetAnnotations(); ann != nil {
if v := ann[fissionSourceTypeAnnotation]; v != "" {
sourceType = v
}
}
```
Поле возвращается в JSON ответе:
```json
{
"name": "my-func",
"code": "...",
"source_type": "archive",
...
}
```
---
## 5. Frontend — submitCreate
До v1.3.45 всегда отправлял JSON с полем `code`.
Теперь определяет режим по видимости `c-archive-area`:
```javascript
var archiveArea = document.getElementById('c-archive-area');
var isArchiveMode = archiveArea && archiveArea.style.display !== 'none';
var archiveFile = isArchiveMode ? document.getElementById('c-archive-file').files[0] : null;
```
**Если archive mode и файл выбран** → FormData:
```javascript
var fd = new FormData();
fd.append('name', name);
fd.append('language', lang);
// ... остальные поля ...
fd.append('archive', archiveFile);
fetch(API_BASE + '/functions', { method: 'POST', headers: authHeaders(), body: fd });
```
**Иначе** → старый JSON путь без изменений.
---
## 6. Frontend — openEdit
До v1.3.45 сбрасывал режим на основе `fn.code !== ''`, что было неправильно:
`extractPackageSourceCode` умеет доставать код даже из архивных функций через URL,
поэтому `fn.code` всегда был непустым.
Теперь использует `fn.source_type`:
```javascript
if (fn.source_type === 'archive') {
setCodeMode('e', 'archive');
} else {
setCodeMode('e', 'code');
}
```
**Поведение:**
- Функция создана через код → открывается редактор кода с текущим кодом
- Функция создана из архива → открывается archive area с file input (пустым)
- Старые функции без аннотации → `source_type: "code"` по умолчанию → code mode
---
## 7. Frontend — submitEdit
До v1.3.45 всегда отправлял `PUT /functions/:name/code` с JSON.
Теперь определяет режим аналогично submitCreate:
```javascript
var archiveArea = document.getElementById('e-archive-area');
var isArchiveMode = archiveArea && archiveArea.style.display !== 'none';
var archiveFile = isArchiveMode ? document.getElementById('e-archive-file').files[0] : null;
```
**Если archive mode и файл выбран** → `PUT /functions/:name/archive` с FormData:
```javascript
fd.append('timeout', String(parseTimeout(...)));
fd.append('archive', archiveFile);
fetch(...'/archive', { method: 'PUT', headers: authHeaders(), body: fd });
```
**Иначе** → старый `PUT /functions/:name/code` с JSON.
**Важно:** если пользователь открыл функцию в archive mode но не выбрал новый файл —
`archiveFile` будет `null` и код уйдёт по JSON-пути (отправит пустой код).
Это expected behaviour: пользователь должен явно выбрать файл для обновления архива.
---
## 8. Исправление версии в UI
В `index.html` номер версии присутствует в **двух** местах:
- Строка 103: верхний правый угол (`<div>` в header)
- Строка 403: блок "Помощь" (`<span>` в help modal)
До v1.3.45 оба места обновлялись несинхронно — строка 103 отставала.
Начиная с v1.3.45 оба места обновляются одновременно через `multi_replace_string_in_file`.
---
## 9. Проблема с imagePullPolicy
При первом деплое v1.3.43 (после исправления версии) UI показывал `v1.3.40`.
Причина: `imagePullPolicy: IfNotPresent` — K8s имел образ `v1.3.43` в кэше
и не тянул обновлённый образ с тем же тегом.
**Решение:** менять тег при каждой сборке (v1.3.43 → v1.3.44 → v1.3.45).
Это принципиальное правило: **никогда не пересобирать образ с тем же тегом**.
---
## 10. Константы и аннотации (handlers.go)
```go
const maxArchiveUploadSize = 32 << 20 // 32 MB
const fissionSourceTypeAnnotation = "fission-console/source-type"
```
Существующие константы не изменялись:
```go
const maxCodeSize = 1 << 20 // 1 MB — только для inline кода
const defaultFunctionInvokeTimeout = 60 * time.Second
```