Files
sless/doc/decisions/funcs_console_editor.md
T
Naeel c762047234 fix(builder/go1.23): add require sless/fn/handler to server/go.mod at build time
go.work replace rule requires explicit require directive in server/go.mod.
Patch appended at kaniko build time - no base image rebuild needed.
2026-03-22 17:28:40 +03:00

233 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# План: веб-редактор функций в funcs-console
Создано: 2026-03-22
---
## Что добавляем
Три фичи в `https://sless.kube5s.ru/funcs/<namespace>`:
1. **Создание функции** — кнопка «+ Новая функция», форма, сохранение
2. **Редактирование кода** — встроенный редактор (CodeMirror), сохранение = загрузка нового кода
3. **Запуск функции** — кнопка «▶ Запустить», поле ввода JSON event, вывод ответа
---
## Текущее состояние (что уже есть)
| Компонент | Есть |
|-----------|------|
| Листинг функций | ✅ |
| Просмотр кода (read-only) | ✅ |
| Enable/disable триггера | ✅ |
| Invoke | ❌ |
| Редактирование | ❌ |
| Создание | ❌ |
---
## Архитектура
### Проблема аутентификации
`/funcs/<ns>` не требует токена пользователя — использует `SLESS_SERVICE_TOKEN`.
Создание/редактирование — **операции записи**, должны быть защищены.
**Решение:** токен передаётся через форму логина:
- При открытии `/funcs/<ns>` без токена → кнопка «Войти», поле ввода токена
- Токен сохраняется в `localStorage` / `sessionStorage`
- Все write-запросы от frontend идут с `Authorization: Bearer <token>` к proxy в funcs-service
- funcs-service proxy **проксирует токен пользователя** к оператору (не serviceToken)
Почему так: оператор уже проверяет JWT структуру. Токен не верифицируется по подписи — это существующее ограничение (trusted perimeter).
---
## Новые маршруты в funcs-service (Go, main.go)
```
POST /funcs/{ns}/api/services — создать сервис (proxy → оператор)
POST /funcs/{ns}/api/services/{name}/code — загрузить код (принимает файлы, делает zip → оператор)
POST /funcs/{ns}/api/services/{name}/invoke — вызвать функцию (proxy → /fn/{ns}/{name})
DELETE /funcs/{ns}/api/services/{name} — удалить сервис (proxy → оператор)
```
Все `/api/` маршруты проксируют **токен из Authorization header** к оператору.
---
## Изменения в Go (main.go)
### 1. Новый handler: `proxyServiceCreate`
```go
// POST /funcs/{ns}/api/services
// Принимает JSON {name, runtime, entrypoint, memory_mb, env_vars}
// Проксирует токен из Authorization header → оператор
```
### 2. Новый handler: `proxyCodeUpload`
```go
// POST /funcs/{ns}/api/services/{name}/code
// Принимает multipart: несколько файлов (name + content)
// Создаёт zip в памяти → POST /v1/namespaces/{ns}/services/{name}/upload
// Проксирует токен из Authorization header
```
Почему zip в памяти: браузер не может создать zip напрямую без JSZip.
Альтернатива: использовать JSZip на фронте → отправить binary zip → проще.
**Выбор: JSZip на фронте** — проще proxy (просто forward binary), меньше Go кода.
### 3. Новый handler: `proxyInvoke`
```go
// POST /funcs/{ns}/api/services/{name}/invoke
// Тело: JSON event от пользователя
// Проксирует → POST /fn/{ns}/{name} (публичный endpoint, без токена)
// Возвращает ответ функции
```
### 4. Расширение роутера в `handleFuncsNS`
```go
case "api":
handleAPI(w, r, operatorURL, externalURL, ns, parts[2:])
```
---
## Изменения в HTML/JS (index.html)
### Зависимости (CDN, добавить в `<head>`)
```html
<!-- CodeMirror 6 — легковесный редактор -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/codemirror.min.js"></script>
<!-- JSZip — создание zip в браузере -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
```
Почему CodeMirror а не Monaco: Monaco тяжёлый (~3MB), подключается через AMD loader.
CodeMirror 6 — лёгкий, простой CDN, достаточен для подсветки Python/JS/Go.
### Фича 1: Авторизация
```
[header] sless / sless-mu01 [⚙ Токен: ________] [Войти] [↻ обновить]
```
- Если токен в localStorage → подставляем в заголовок сразу
- Иначе — поле ввода видно
- Токен валидируется структурно на JS (3 части, exp > now)
### Фича 2: Кнопка «+ Сервис»
```
[header] ... [+ Сервис] [↻ обновить]
```
Клик → **модальное окно**:
```
Имя: [____________]
Runtime: [python3.11 ▾]
Entrypoint: [handler.handle]
Memory (MB): [128]
Env vars: [KEY=VALUE, по одной строке]
[+ ещё одна строка]
[Отмена] [Создать]
```
После создания (201) → открывается редактор кода для этой функции.
### Фича 3: Редактор кода
На каждой карточке функции — кнопка «✎ Редактировать» (рядом с expand).
При клике:
1. Загружается текущий код через `/funcs/{ns}/source/{fn}?kind=service`
2. Открывается **inline-редактор** под карточкой (или modal — обсудить)
3. CodeMirror с подсветкой по runtime
Интерфейс редактора:
```
┌─ handler.py ──────────────────────────── [+ файл] [✕] ─┐
│ def handle(event): │
│ return {"ok": True} │
│ │
│ │
└──────────────────────────────────────────────────────────┘
┌─ requirements.txt ─────────────────────── [✕] ──────────┐
│ psycopg2-binary==2.9.9 │
└──────────────────────────────────────────────────────────┘
[Сохранить и пересобрать] [Отмена]
```
Сохранение:
1. JSZip.file(name, content) для каждого открытого файла
2. zip.generateAsync({type:"blob"}) → FormData → POST `/funcs/{ns}/api/services/{name}/code`
3. Proxy → оператор upload → kaniko re-build
4. После 200 → карточка показывает "Building..."
### Фича 4: Запуск функции
На каждой карточке сервиса (kind=service, phase=Ready) — кнопка «▶ Запустить».
Клик → **inline панель** под карточкой:
```
Event JSON:
┌──────────────────────────────────────────────────────────┐
│ {"name": "world"} │
└──────────────────────────────────────────────────────────┘
[▶ Отправить]
Ответ (200, 34ms):
┌──────────────────────────────────────────────────────────┐
│ {"hello": "world"} │
└──────────────────────────────────────────────────────────┘
```
Запрос идёт напрямую с браузера на `/fn/{ns}/{name}` (публичный endpoint, без токена).
Ответ показывается с highlight.js.
---
## Порядок реализации
| # | Шаг | Файл | Сложность |
|---|-----|------|-----------|
| 1 | Добавить `/api/` роуты в `handleFuncsNS` | `main.go` | низкая |
| 2 | `proxyServiceCreate` handler | `main.go` | низкая |
| 3 | `proxyCodeUploadForward` handler (forward binary zip) | `main.go` | низкая |
| 4 | `proxyInvoke` handler | `main.go` | минимальная |
| 5 | Форма авторизации (localStorage токен) | `index.html` | низкая |
| 6 | Кнопка + Сервис + модальное окно создания | `index.html` | средняя |
| 7 | Встроенный редактор CodeMirror + JSZip upload | `index.html` | средняя |
| 8 | Панель invoke | `index.html` | низкая |
| 9 | Пересобрать образ funcs-service + деплой | Makefile/deploy | ~5 мин |
| 10 | Smoke-test: создать → редактировать → запустить | браузер | ~5 мин |
**Порядок важен:** сначала backend proxy (1-4), потом UI (5-8).
---
## Что НЕ делаем (scope)
- Удаление функции через UI — не заявлено, пропускаем
- Редактирование job-style функций (FunctionJob) — только sless_service
- История версий кода — S3 уже хранит последнюю, версионирование не реализовано
- Управление триггерами (создание/удаление) — уже есть enable/disable, этого достаточно
- Real-time логи — отдельная задача
---
## Связанные файлы
- `services/funcs/main.go`
- `services/funcs/index.html`
- `services/funcs/Dockerfile`
- `deployments/k8s/funcs-service.yaml`