- 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
240 lines
9.7 KiB
Markdown
240 lines
9.7 KiB
Markdown
# 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
|
||
```
|