# 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: верхний правый угол (`