go.work replace rule requires explicit require directive in server/go.mod. Patch appended at kaniko build time - no base image rebuild needed.
233 lines
11 KiB
Markdown
233 lines
11 KiB
Markdown
# План: веб-редактор функций в 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`
|