ui: archive upload mode + lint stub, fix stress warmup output, add descriptions

This commit is contained in:
“Naeel”
2026-05-03 17:14:25 +04:00
parent 2af39ff3e2
commit e9853a4bc0
6 changed files with 436 additions and 17 deletions
+162
View File
@@ -1,5 +1,167 @@
# Логика авторизации — Fission Console
> ⚠️ ЛЕГАСИ (актуально до ~апреля 2026): описание ниже относится к старой схеме с Deck API + JWT.
> Актуальное описание — **выше этого блока**, в разделе "АКТУАЛЬНО (май 2026+)".
---
# АКТУАЛЬНО (май 2026+)
## Что принимает сервер
Токен передаётся одним из двух способов:
- Заголовок `X-Auth-Token: <токен>`
- Заголовок `Authorization: Bearer <токен>`
`X-Auth-Env` — необязателен, по умолчанию `test`.
---
## Типы токенов и маршрутизация (MultiAuthenticator)
Сервер использует `MultiAuthenticator`, который определяет тип по форме токена:
| Форма токена | Authenticator | Описание |
|---|---|---|
| JWT (три части через `.`) | `DeckAuthenticator` или `TestAuthenticator` | Реальный облачный токен или тест-JWT |
| Любая строка ≥6 символов | `DemoAuthenticator` | Demo-логин без внешних запросов |
| Строка <6 символов | — | Ошибка 401 |
---
## DemoAuthenticator (текущий основной режим)
Принимает **любую строку ≥6 символов** как логин. Не ходит во внешние сервисы.
```
login (≥6 символов)
Sub = UUID v5(login) ← детерминированный, фиксированный UUID namespace
Email = "demo-lo***in" ← маскированный для отображения в UI
Namespace = "fission-" + hex(SHA256(Sub)[:8])
```
Пример:
- login = `livetest@test.local`
- Sub = `uuidV5("livetest@test.local")` = фиксированный UUID
- Namespace = `fission-c3fce59430e41b0f`
Каждый логин → один и тот же namespace (детерминированно).
---
## DeckAuthenticator (production JWT)
Для JWT-токенов из облака NUBES:
```
JWT токен
├─► validateToken → GET {deckAPI}/index.cfm/instances
│ Authorization: Bearer <token>
│ 401 → ошибка | другой → ОК
│ Кэш на 5 минут (sync.Map, ключ "env:token")
└─► identityFromJWT → decode payload (base64url, без верификации подписи)
→ claim "sub" → Sub
→ claim "email" → Email (опционально)
```
Deck API URL по стендам:
- `prod``https://deck-api.ngcloud.ru/api/v1`
- `dev``https://deck-api-dev.ngcloud.ru/api/v1`
- `test``https://deck-api-test.ngcloud.ru/api/v1`
---
## TestAuthenticator (тест-режим, устаревший)
Принимает JWT — декодирует sub из payload. Или строку с `@` — использует как sub напрямую.
Не ходит в Deck API. Включался через `FISSION_TEST_MODE=true`.
> ⚠️ Сейчас не используется — заменён на DemoAuthenticator.
---
## Вычисление namespace: `NamespaceForSub`
```go
func NamespaceForSub(sub string) string {
h := sha256.Sum256([]byte(sub))
return "fission-" + hex.EncodeToString(h[:8]) // 16 hex символов
}
```
Namespace **детерминирован**: одинаковый логин → всегда один namespace.
---
## authMiddleware — что происходит на каждый запрос
```
Входящий HTTP запрос
├─► authTokenFromRequest → X-Auth-Token || Authorization: Bearer
├─► authenticator.Authenticate(token, env) → UserIdentity{Sub, Email}
│ ошибка → 401
├─► NamespaceForSub(Sub) → "fission-XXXXXXXXXXXXXXXX"
├─► nsManager.EnsureUserNS(namespace)
│ → создаёт K8s namespace + RBAC если нет
│ ошибка → 502
└─► контекст запроса: ctxKeyNS=namespace, ctxKeyIdentity=identity
→ handler работает в namespace пользователя
```
---
## POST /auth — явный вход
```
POST /console/api/auth
Body: { "token": "...", "env": "test" }
└─► то же что authMiddleware, возвращает:
{ ok: true, namespace: "fission-...", email: "demo-lo***in" }
```
---
## Итого — полная схема (актуальная)
```
Пользователь передаёт токен/логин
MultiAuthenticator
├── JWT? → DeckAuthenticator (Deck API + JWT decode)
└── нет → DemoAuthenticator (≥6 символов → UUID v5)
UserIdentity { Sub, Email }
NamespaceForSub(Sub) = "fission-" + hex(SHA256(Sub)[:8])
EnsureUserNS → K8s namespace существует
Все хендлеры работают с namespace из context
```
---
# ЛЕГАСИ (до апреля 2026) — не удалять, для анализа
## Что вводит пользователь
В форме логина (`index.html`, overlay `#login-overlay`):
+180
View File
@@ -0,0 +1,180 @@
# Создание функции — текущий flow (май 2026)
## UI (браузер)
### Форма создания (`create-modal`)
Поля в `index.html`:
- `c-lang` — select (python / nodejs / go / ruby / php)
- `c-name` — имя функции
- `c-entry` — entrypoint (заполняется автоматически по языку из `LANG_TEMPLATES`)
- `c-route` — HTTP-путь
- `c-methods` — методы, через запятую
- `c-timeout` — таймаут в секундах
- `c-schedule-enabled` + `c-cron` — cron-расписание (опционально)
- `c-code`**textarea** с исходным кодом функции (единственный способ передать код)
Кнопки рядом с `c-code`:
- `aiCheck(...)` — линтер (POST `/console/api/ai/check`)
- `aiExplain(...)` — объяснение через LLM (POST `/console/api/ai/ask`)
При нажатии «Создать» вызывается `submitCreate()` (`js/functions.js`):
```js
await requestJSON(API_BASE + '/functions', 'POST', {
name, language, entrypoint, route, methods, timeout,
code: document.getElementById('c-code').value // <-- всегда текст
});
```
### Форма редактирования (`edit-modal`)
Аналогична. `submitEdit()` шлёт:
```js
await requestJSON(API_BASE + '/functions/' + name + '/code', 'PUT', {
code: document.getElementById('e-code').value,
timeout: ...
});
```
---
## Backend
### POST /console/api/functions → `handleCreateFunction`
Структура запроса (`model.CreateFunctionRequest`):
```json
{ "name": "fn", "language": "python", "code": "def main(): ...",
"entrypoint": "main", "route": "/ns/fn", "methods": ["GET"],
"timeout": 60, "ttl": "" }
```
Обязательные: `name`, `language` (или `environment`), `code`.
**Путь для интерпретируемых языков** (python / nodejs / ruby / php):
```
code (строка)
→ buildDeployArchive(lang, code)
python/ruby/php: []byte(code) (raw, не zip)
nodejs: BuildJSDeployZip(code) → zip с package.json + main.js
→ buildDeploySpec(ctx, bytes)
→ uploadToStoragesvc() → multipart POST → storagesvc → archiveURL
→ fallback: base64 literal (если storagesvc недоступен)
→ Package.spec.deployment = { type: "url", url: "..." }
или = { type: "literal", literal: "base64..." }
```
**Путь для Go**:
```
code
→ BuildGoSourceZip(code) → zip с исходником
→ uploadToStoragesvc → archiveURL
→ Package.spec.source = { ... } (source package, компилируется builder job)
Package.spec.buildcommand = "build"
```
После создания Package → создаётся Function → HTTPTrigger.
При ошибке на любом шаге — откат (best-effort DELETE предыдущих объектов).
### PUT /console/api/functions/{name}/code → `handleUpdateFunctionCode`
```json
{ "code": "...", "timeout": 60 }
```
Обязателен: `code`.
Алгоритм:
1. Читает текущую Function → берёт `fission-console/language` из аннотации
2. `buildDeployArchive(lang, code)` → байты
3. Создаёт **новый Package** с именем `{name}-pkg-{timestamp_base36}`
4. Обновляет Function.spec.package.packageref на новый пакет
5. Удаляет старый Package (best-effort)
> Почему новый Package, а не обновление старого:
> executor кэширует function-service по `functionUid` — не видит изменений в том же Package.
> Новое имя гарантирует cache miss.
---
## Линтер (ai/check) и LLM (ai/ask)
### POST /console/api/ai/check
```json
{ "language": "python", "code": "def main(): ..." }
```
Работает **только со строкой кода**. Запускает:
- python: `python3 -m py_compile <tmpfile.py>`
- nodejs: `node --check <tmpfile.js>`
- ruby: `ruby -c <tmpfile.rb>`
- php: `php -l <tmpfile.php>`
- go: `go/parser.ParseFile(...)` прямо в процессе (без tmpfile)
Временный файл создаётся в `os.TempDir()`, удаляется через `defer`.
### POST /console/api/ai/ask
Отправляет код в LLM (объяснение, анализ). Детали в `ai_prompts.go`.
---
## Что сейчас НЕ поддерживается
- **Загрузка архива (.zip) из файла** — ни в UI, ни в API
- Архив можно только скачать (через `extractPackageSourceCode``fetchPackageArchive`)
---
## Planned: загрузка архива
### API
Добавить в `CreateFunctionRequest` и `UpdateCodeRequest` поле `archive_b64 string`.
Логика в handler: если `archive_b64 != ""` — пропустить `buildDeployArchive`,
сразу `uploadToStoragesvc(base64.Decode(archive_b64))`.
Валидация:
- magic bytes `PK\x03\x04` (zip)
- максимум 10MB (против 1MB для кода)
### UI
Раздельные режимы в форме — кнопки-переключатели «✏️ Код» / «📦 Архив»:
- режим «Код» — textarea (как сейчас)
- режим «Архив» — `<input type="file" accept=".zip">`, textarea скрыта
В `submitCreate` / `submitEdit``FileReader.readAsDataURL(file)` → base64 → поле `archive_b64`.
### Линтер и LLM для архива
Проблема: линтер (`ai/check`) и LLM (`ai/ask`) принимают `code` — строку.
Архив — zip из нескольких файлов. Варианты:
**Вариант A — на бэке, при загрузке**
После распаковки архива (уже есть `decodeArchiveBytesToSource` в `package.go`):
- Извлечь главный файл (entrypoint)
- Прогнать через линтер
- Результат вернуть в том же ответе на create/update (поле `lint_result`)
Плюсы: не меняет контракт линтера, нет доп. round-trip.
Минусы: линтится только один файл из архива.
**Вариант B — на фронте, до отправки**
`FileReader.readAsText(file)` — не работает для zip напрямую.
Можно использовать `JSZip` (npm/CDN) — распаковать в браузере → взять главный файл → передать в `ai/check`.
Плюсы: линтинг до загрузки, быстрая обратная связь.
Минусы: добавляется зависимость (JSZip ~100KB).
**Вариант C — гибрид (рекомендован)**
1. Фронт: при выборе архива — использовать JSZip, найти entrypoint-файл по языку,
показать его в readonly-textarea, кнопки линтера и LLM работают с этим текстом.
2. Отправка: `archive_b64` (весь zip) идёт на бэк как есть.
3. Бэк (опционально): после сохранения прогнать `decodeArchiveBytesToSource` → линтер,
вернуть предупреждение если нашёл ошибки.
Так пользователь видит и код и результат линтера до деплоя, и полный архив уходит без изменений.