doc: add full function lifecycle description (S3, lint, env, runtime)
This commit is contained in:
@@ -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. Читает файл из `<input id="ca-archive-file">`
|
||||
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 <NS> console-python-env --type=json \
|
||||
-p '[{"op":"replace","path":"/spec/runtime/image","value":"naeel/fission-python-env:v1.0"}]'
|
||||
```
|
||||
|
||||
Имя environment всегда: `console-<lang>-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 <function>(...) должна быть определена в файле
|
||||
```
|
||||
|
||||
### 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 вызовы
|
||||
```
|
||||
Reference in New Issue
Block a user