From 78f72c593f21b0610a2d4de067fafe98a5da00e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Mon, 4 May 2026 11:13:27 +0400 Subject: [PATCH] doc: add full function lifecycle description (S3, lint, env, runtime) --- doc/descriptions/function-lifecycle-full.md | 355 ++++++++++++++++++++ 1 file changed, 355 insertions(+) create mode 100644 doc/descriptions/function-lifecycle-full.md diff --git a/doc/descriptions/function-lifecycle-full.md b/doc/descriptions/function-lifecycle-full.md new file mode 100644 index 0000000..e43b39c --- /dev/null +++ b/doc/descriptions/function-lifecycle-full.md @@ -0,0 +1,355 @@ +# Полный жизненный цикл функции в Fission Console + +Актуально: **v1.3.56**, май 2026 + +--- + +## 1. Два способа создания функции + +| Способ | Content-Type | Обработчик | +|--------|-------------|------------| +| **Inline-код** (редактор) | `application/json` | `handleCreateFunction` → `handleCreateFunctionFromCode` | +| **Архив** (zip-файл) | `multipart/form-data` | `handleCreateFunction` → `handleCreateFunctionFromArchive` | + +Определяется автоматически по заголовку `Content-Type` в `handleCreateFunction` (handlers.go ~строка 343). + +--- + +## 2. Создание из inline-кода (шаг за шагом) + +### 2.1 UI → Backend + +**Файл:** `console/ui/js/functions.js` → `submitCreateFunction()` + +1. Читает поля: `name`, `language`, `entrypoint`, `route`, `methods`, `timeout`, `code` (из CodeMirror) +2. Отправляет `POST /console/api/functions` с `Content-Type: application/json` + +### 2.2 Backend обработка + +**Файл:** `console/internal/api/handlers.go` → `handleCreateFunction` (строка ~343) + +``` +POST /console/api/functions (JSON) + ↓ +Валидация имени (regex ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$, max 57 символов) + ↓ +EnsureUserNS → создаёт K8s namespace если не существует + ↓ +EnsureEnvironment → создаёт Fission Environment CRD если не существует + ↓ +buildDeployArchive(lang, code) → упаковывает код в байты + ↓ +buildDeploySpec(ctx, bytes) → uploadToStoragesvc → S3 + ↓ +Создаёт Fission Package CRD (type: url → S3) + ↓ +Создаёт Fission Function CRD (ссылается на Package) + ↓ +Создаёт Fission HTTPTrigger CRD (роут) + ↓ +Аннотация fission-console/source-type = "code" +``` + +### 2.3 buildDeployArchive — упаковка кода + +**Файл:** `console/internal/api/handlers.go` → `buildDeployArchive()` + +| Язык | Что происходит | +|------|---------------| +| Python | код → сырые байты (*.py файл) | +| Node.js | код → zip с `package.json` + `main.js` (ESM wrapper) | +| PHP | код → zip с `main.php` | +| Ruby | код → zip с `handler.rb` | +| Go | отдельный путь: `BuildGoSourceZip` → source package → builder job | + +### 2.4 Entrypoint по умолчанию + +**Файл:** `console/internal/runtime/` → `DefaultEntrypoint(lang)` + +| Язык | Entrypoint по умолчанию | +|------|------------------------| +| python | `user.main` | +| nodejs | `main` | +| go | `Handler` | +| php | `main` | +| ruby | `main` | + +--- + +## 3. Создание из архива (шаг за шагом) + +### 3.1 UI → Backend + +**Файл:** `console/ui/js/fn-archive.js` → `submitCreateArchive()` + +1. Читает поля: `name`, `language`, `entrypoint`, `route`, `methods`, `timeout` +2. Читает файл из `` +3. **Валидация на UI:** файл должен заканчиваться на `.zip` — иначе ошибка до отправки +4. Отправляет `POST /console/api/functions` с `Content-Type: multipart/form-data` + +### 3.2 Backend обработка + +**Файл:** `console/internal/api/handlers.go` → `handleCreateFunctionFromArchive()` (строка ~1302) + +``` +POST /console/api/functions (multipart/form-data) + ↓ +ParseMultipartForm (лимит 32 MB) + ↓ +Валидация имени + ↓ +r.FormFile("archive") → читает байты архива + ↓ +⛔ ПРОВЕРКА magic bytes: первые 2 байта должны быть 0x50 0x4B (PK = zip) + Если нет → HTTP 400 "загруженный файл не является валидным zip-архивом" + ↓ +EnsureUserNS + ↓ +EnsureEnvironment (по полю "language") + ↓ +buildDeploySpec(ctx, archiveBytes) → uploadToStoragesvc → S3 + ↓ +Создаёт Fission Package CRD (type: url → S3) + ↓ +Создаёт Fission Function CRD + ↓ +Создаёт Fission HTTPTrigger CRD + ↓ +Аннотация fission-console/source-type = "archive" + ↓ +Аннотация fission-console/archive-file = <оригинальное имя файла> +``` + +--- + +## 4. Загрузка в S3 через storagesvc + +**Файл:** `console/internal/api/handlers.go` → `uploadToStoragesvc()` + +``` +Входные данные: []byte (байты zip-архива) + ↓ +Строим multipart/form-data вручную: + boundary = "fission" + UnixNano + поле: name="uploadfile", filename="archive.zip" + Content-Type: application/octet-stream + тело: байты архива + ↓ +POST http://storagesvc.fission.svc.cluster.local/v1/archive + заголовок X-File-Size: <размер байт> + ↓ +Storagesvc сохраняет в S3 (bucket: sless-functions) + ↓ +Ответ: {"id": "fission/UUID"} + ↓ +Возвращаем URL: http://storagesvc.../v1/archive?id=fission/UUID +``` + +**Если storagesvcURL не задан** → fallback на `type: literal` (base64 в etcd). Нежелательно для больших файлов. + +**Переменная окружения:** `STORAGESVC_URL=http://storagesvc.fission.svc.cluster.local` + +### 4.1 Удаление из S3 + +При удалении функции → `deleteFromStoragesvc()`: +``` +DELETE http://storagesvc.../v1/archive?id=fission/UUID +``` +Best-effort: ошибка логируется но не прерывает удаление Function/Package CRD. + +--- + +## 5. Environment — КРИТИЧЕСКИ ВАЖНО + +**Файл:** `console/internal/fission/environment.go` → `EnsureEnvironment()` +**Конфиг образов:** `console/internal/model/types.go` → `LangEnvMap` + +### 5.1 Lazy creation + +Environment создаётся **только когда пользователь создаёт первую функцию** на языке X в своём namespace. Не заранее — экономия ресурсов. + +### 5.2 Текущие образы (АКТУАЛЬНО) + +```go +var LangEnvMap = map[string]LangEnvDef{ + "python": {Image: "naeel/fission-python-env:v1.0"}, // ⛔ НЕ МЕНЯТЬ НА ОФИЦИАЛЬНЫЙ + "nodejs": {Image: "ghcr.io/fission/node-env"}, + "go": {Image: "ghcr.io/fission/go-env", BuilderImage: "naeel/go-builder-fast:v1"}, + "php": {Image: "ghcr.io/fission/php-env"}, + "ruby": {Image: "ghcr.io/fission/ruby-env"}, +} +``` + +### 5.3 ⛔⛔⛔ PYTHON — ТОЛЬКО НАШ ОБРАЗ + +`naeel/fission-python-env:v1.0` содержит патч `inspect.signature`: + +```python +def _count_required_params(func): + # inspect.signature → считает обязательные аргументы + +def userfunc_call(self, *args, **kwargs): + required = _count_required_params(self.userfunc) + if required == 0: return self.userfunc() # def main(): + elif required == 1: return self.userfunc(request) # def main(request): + elif required >= 2: return self.userfunc(request, {}) # def main(event, context): +``` + +**Официальный `ghcr.io/fission/python-env`** вызывает `userfunc()` без аргументов → `TypeError: missing 2 required positional arguments` → HTTP 500 для любой функции `def main(event, context)`. + +### 5.4 Если environment уже создан с неправильным образом + +```bash +kubectl patch environment -n console-python-env --type=json \ + -p '[{"op":"replace","path":"/spec/runtime/image","value":"naeel/fission-python-env:v1.0"}]' +``` + +Имя environment всегда: `console--env` (например `console-python-env`). + +--- + +## 6. Линтер (lint-archive) + +**Файл:** `console/internal/api/lint_archive.go` → `handleLintArchive()` + +**Эндпоинт:** `POST /console/api/ai/lint-archive` + +### 6.1 Когда вызывается + +Из UI при выборе файла в форме архива (`fn-archive.js` → `lintArchiveFile(prefix)`). +Поля: `archive` (zip), `entrypoint` (опционально), `language` (опционально). + +### 6.2 Порядок проверок + +``` +1. Размер архива ≤ 100 KB (maxArchiveBytes) +2. Суммарный распакованный размер ≤ 100 KB (защита от zip bomb) +3. Синтаксический линтер по каждому файлу: + .py → python3 -m py_compile + .js → node --check + .rb → ruby -c + .php → php -l +4. Проверка language vs расширения файлов в архиве: + python → нужен хотя бы один .py + nodejs → нужен хотя бы один .js + ...и т.д. +5. Проверка entrypoint (формат "module.function"): + → файл module.py должен быть в архиве + → def (...) должна быть определена в файле +``` + +### 6.3 Ответ + +```json +{ + "results": [ + {"file": "demo.py", "ok": true}, + {"file": "utils.py", "ok": false, "output": "SyntaxError: ..."}, + {"file": "язык", "ok": false, "output": "выбран python, но .py файлов нет"}, + {"file": "entrypoint", "ok": false, "output": "demo.py найден, но def main не определён"} + ], + "ok": false +} +``` + +--- + +## 7. Fission: как функция выполняется (runtime) + +``` +Пользователь вызывает HTTP URL роута + ↓ +Fission Router (JWT auth если включён) + ↓ +Executor (poolmgr) — выбирает под из пула + ↓ +Fetcher sidecar — скачивает архив из S3: + GET http://storagesvc.../v1/archive?id=fission/UUID + Распаковывает zip → /userfunc/ + (для inline Python: кладёт .py как /userfunc/user.py) + ↓ +Python runtime pod специализируется: + POST /specialize на порт 8888 пода + server.py импортирует module из /userfunc/ + ↓ +Вызов функции: + _count_required_params(userfunc) → 0/1/2 аргументов + 0 → userfunc() + 1 → userfunc(request) + 2 → userfunc(request, {}) +``` + +--- + +## 8. Namespace пользователя + +Каждый пользователь → отдельный K8s namespace. + +| Пользователь | Namespace | +|-------------|-----------| +| chiken | `fission-54a1d63ee5e0921b` | +| livetest@test.local | `fission-199e39e44b809ec1` | + +Namespace создаётся при первом обращении через `EnsureUserNS()`. + +Имя environment всегда `console-python-env` (не уникально на пользователя) — каждый пользователь имеет СВОЙ environment в своём namespace. + +--- + +## 9. Роут функции + +Формат по умолчанию: `/<последние 12 символов namespace>/<имя функции>` + +Пример: namespace `fission-54a1d63ee5e0921b` → роут `/d63ee5e0921b/funcname` + +--- + +## 10. Аннотации на Function CRD + +| Аннотация | Значение | +|-----------|---------| +| `fission-console/source-type` | `code` или `archive` | +| `fission-console/archive-file` | оригинальное имя загруженного файла (только для архивов) | +| `fission-console/expires-at` | Unix timestamp протухания (если задан TTL) | + +--- + +## 11. Что НЕ ДОЛЖНО происходить (зафиксированные баги) + +| Баг | Причина | Последствие | +|-----|---------|-------------| +| `ghcr.io/fission/python-env` в LangEnvMap | Забыли про наш патч | `def main(event, context)` → 500 | +| Загрузка `.py` файла вместо `.zip` | UI не проверял расширение | Fission не может распаковать → 500 | +| 0 байт в S3 | Файл не zip (magic bytes ≠ PK) | Fetcher не может распаковать → 500 | + +--- + +## 12. Файловая структура кода (консоль) + +``` +console/ + cmd/server/ — точка входа (main.go) + internal/ + api/ + handlers.go — HTTP handlers: CRUD функций, invoke, edit + lint_archive.go — POST /ai/lint-archive + ai_check.go — LLM-анализ кода через OpenAI-совместимый API + server.go — Server struct, маршрутизация + auth/ — JWT/Basic auth middleware + fission/ + environment.go — EnsureEnvironment, CleanupEnvironmentIfUnused + namespace.go — EnsureUserNS + gvr.go — GVR константы для K8s dynamic client + model/ + types.go — LangEnvMap ⚠️ ОБРАЗЫ ЯЗЫКОВ ЗДЕСЬ + runtime/ + python.go — DefaultEntrypoint, BuildPythonZip + nodejs.go — BuildJSDeployZip + go.go — BuildGoSourceZip + ui/ + js/ + functions.js — создание/редактирование inline-функций + fn-archive.js — создание из архива + fn-edit-archive.js — редактирование архивной функции + app.js — общая логика, auth, API вызовы +```