# План: веб-редактор функций в funcs-console Создано: 2026-03-22 --- ## Что добавляем Три фичи в `https://sless.kube5s.ru/funcs/`: 1. **Создание функции** — кнопка «+ Новая функция», форма, сохранение 2. **Редактирование кода** — встроенный редактор (CodeMirror), сохранение = загрузка нового кода 3. **Запуск функции** — кнопка «▶ Запустить», поле ввода JSON event, вывод ответа --- ## Текущее состояние (что уже есть) | Компонент | Есть | |-----------|------| | Листинг функций | ✅ | | Просмотр кода (read-only) | ✅ | | Enable/disable триггера | ✅ | | Invoke | ❌ | | Редактирование | ❌ | | Создание | ❌ | --- ## Архитектура ### Проблема аутентификации `/funcs/` не требует токена пользователя — использует `SLESS_SERVICE_TOKEN`. Создание/редактирование — **операции записи**, должны быть защищены. **Решение:** токен передаётся через форму логина: - При открытии `/funcs/` без токена → кнопка «Войти», поле ввода токена - Токен сохраняется в `localStorage` / `sessionStorage` - Все write-запросы от frontend идут с `Authorization: Bearer ` к 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, добавить в ``) ```html ``` Почему 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`