421 lines
20 KiB
Markdown
421 lines
20 KiB
Markdown
# 2026-04-15 — Thinking Log
|
||
|
||
**Агент:** GitHub Copilot (Claude Opus 4.6)
|
||
|
||
---
|
||
|
||
## Сессия: Fixup invoke auth в fission-console
|
||
|
||
### Контекст на начало сессии
|
||
|
||
Console (fission-console) — веб-UI для управления Fission serverless functions.
|
||
Был полностью реализован: CRUD (create/get/update/delete), UI с модалами, unit-тесты,
|
||
Docker-образ, k8s deployment, ingress. Всё работало через `https://fission.kube5s.ru/console/`.
|
||
|
||
**Единственная нерешённая проблема:** invoke функции через console API возвращал
|
||
`401 unauthorized: malformed token` — роутер Fission требует авторизацию.
|
||
|
||
### Анализ проблемы
|
||
|
||
Invoke flow: console → POST к роутеру Fission → роутер вызывает функцию.
|
||
|
||
Роутер Fission (`router.fission.svc.cluster.local`) сконфигурирован с auth:
|
||
- `AUTH_USERNAME` / `AUTH_PASSWORD` — из секрета `router`
|
||
- `JWT_SIGNING_KEY` — из секрета `router`
|
||
- ConfigMap `feature-config` содержит: `auth.enabled: true`, `auth.authUriPath: "/auth/login"`, `auth.jwtExpiryTime: 120`
|
||
|
||
Значит роутер использует **собственный JWT**, а не K8s ServiceAccount tokens.
|
||
|
||
### Первая попытка: SA token (неудачная)
|
||
|
||
Изначально решил использовать ServiceAccount token из пода:
|
||
- Файл `/var/run/secrets/kubernetes.io/serviceaccount/token` автоматически монтируется в любой под
|
||
- Добавил `saTokenPath` в server struct, метод `readSAToken()`
|
||
- При invoke читал токен и ставил `Authorization: Bearer <token>`
|
||
|
||
Результат: `401 unauthorized: key is of invalid type` — роутер пытался валидировать K8s JWT
|
||
своим signing key (HS256), а K8s SA token — RS256. Не совместимы.
|
||
|
||
### Исследование auth-механизма роутера
|
||
|
||
Прочитал секрет `router`:
|
||
- username: `admin`
|
||
- password: `7XG1lSg0EFqPLE4pf3He`
|
||
- jwtSigningKey: `FWT3rdVUwfUn08tkC3lt`
|
||
|
||
Проверил endpoint `/auth/login`:
|
||
```
|
||
POST http://router.fission.svc.cluster.local/auth/login
|
||
Body: {"username":"admin","password":"..."}
|
||
Response 201: {"accesstoken":"eyJ...","tokentype":"Bearer"}
|
||
```
|
||
|
||
Проверил invoke с полученным JWT:
|
||
```
|
||
GET http://router.fission.svc.cluster.local/auth-test2
|
||
Header: Authorization: Bearer eyJ...
|
||
Response 200: hello
|
||
```
|
||
|
||
**Работает!** Роутер принимает свой JWT.
|
||
|
||
### Вторая попытка: JWT login (успешная)
|
||
|
||
**Решение:** console при invoke делает POST `/auth/login` к роутеру, получает JWT, кеширует,
|
||
использует для вызова функции.
|
||
|
||
Изменения в `main.go`:
|
||
1. Добавил поля в server struct: `authUser`, `authPass`, `tokenMu sync.Mutex`, `cachedJWT`, `tokenExpAt`
|
||
2. Новый метод `getRouterToken()`:
|
||
- Если `authUser`/`authPass` заданы → делает POST `/auth/login`
|
||
- Кеширует JWT на 100 сек (JWT expiry = 120 сек, берём с запасом)
|
||
- Fallback на SA token если login не удался
|
||
3. В `handleInvokeFunction` вызывает `getRouterToken()` и ставит `Authorization: Bearer <token>`
|
||
4. Env переменные `FISSION_AUTH_USERNAME` / `FISSION_AUTH_PASSWORD` берутся из секрета `router`
|
||
|
||
Изменения в `deploy/console.yaml`:
|
||
- Добавил env `FISSION_AUTH_USERNAME` и `FISSION_AUTH_PASSWORD` с `secretKeyRef` из секрета `router`
|
||
|
||
### Баг: 201 vs 200
|
||
|
||
Первый деплой с JWT login не работал: в логах видно что login возвращает **201 Created**
|
||
(не 200 OK), а мой код проверял строго `resp.StatusCode != http.StatusOK`.
|
||
JWT получался, но отбрасывался.
|
||
|
||
Фикс: `if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {`
|
||
|
||
### Баг: кешированный Docker-образ
|
||
|
||
После первого `docker push naeel/fission-console:v0.2.0` без JWT, потом пересобрал
|
||
с JWT и запушил **тот же тег** v0.2.0. Но нод кластера использовал старый образ
|
||
(imagePullPolicy по умолчанию = IfNotPresent). SHA не совпадали.
|
||
|
||
Решение: начал использовать инкрементальные теги (v0.2.1, v0.2.2).
|
||
|
||
### Финальный результат
|
||
|
||
E2E тест через ingress:
|
||
```
|
||
=== create ===
|
||
{"httptrigger":"final-test-route","name":"final-test","package":"final-test-pkg","route":"/final-test"}
|
||
|
||
=== invoke ===
|
||
{"invoke_url":"http://router.fission.svc.cluster.local/final-test","latency_ms":138,"response_raw":"invoke works!","status":200}
|
||
|
||
=== delete ===
|
||
{"deleted":true,"name":"final-test","package":"final-test-pkg"}
|
||
```
|
||
|
||
**Все 5 тестов пройдены.** Commit `8920ba1` → `feat/console`.
|
||
|
||
### Обновление unit-тестов
|
||
|
||
Тест `TestInvokeFunctionWithJWTAuth`:
|
||
- Поднимает mock HTTP server, который на `/auth/login` возвращает `{"accesstoken":"fake-jwt-token-xyz"}`
|
||
- На другие пути проверяет `Authorization` header
|
||
- Создаёт функцию через CRUD, вызывает invoke
|
||
- Проверяет что JWT был получен и отправлен
|
||
|
||
### Итоговая архитектура auth flow
|
||
|
||
```
|
||
User → POST /console/api/functions/NAME/invoke
|
||
↓
|
||
Console backend:
|
||
1. GET trigger для NAME → route, methods
|
||
2. getRouterToken():
|
||
- cached JWT есть и не expired? → используем
|
||
- иначе POST /auth/login → получаем JWT, кешируем на 100s
|
||
3. GET/POST router_url + route
|
||
Header: Authorization: Bearer <JWT>
|
||
↓
|
||
Fission Router (валидирует JWT)
|
||
↓
|
||
Function pod → response
|
||
↓
|
||
Console → User (JSON: status, latency_ms, invoke_url, response_raw)
|
||
```
|
||
|
||
---
|
||
|
||
## Сессия: Обновление правил репозиториев
|
||
|
||
Добавлены правила работы с файловой системой и SSH:
|
||
- sless: обновлён `.github/copilot-instructions.md` — добавлена секция про маппинг путей,
|
||
разрешение редактировать локально, обязательность SSH для команд
|
||
- fission: создан `.github/copilot-instructions.md` с аналогичными правилами
|
||
|
||
Причина: VPN может быть активен локально, вызывая сбои при прямом выполнении команд.
|
||
Монтирование sshfs делает локальное редактирование файлов эквивалентным редактированию на ВМ.
|
||
|
||
---
|
||
|
||
## Сессия: JS invoke timeouts в UI (root cause + fix)
|
||
|
||
### Симптом
|
||
|
||
- Все JS invoke через UI (`/console/api/functions/{name}/invoke`) падали по timeout.
|
||
- В router логах: `roundtripper timeout (60s)`/`error sending request to function`.
|
||
- В executor логах specialization для node окружения могла проходить, но ответ функции не возвращался.
|
||
|
||
### Ключевые находки
|
||
|
||
1. Node runtime работал через `POST /v2/specialize`.
|
||
2. При `functionName=main` runtime строил путь загрузки как:
|
||
- `/userfunc/deployarchive/main`
|
||
3. В реальном поде `/userfunc/deployarchive` был **файлом** (literal payload), а не директорией.
|
||
4. После перевода на пустой entrypoint (`functionName=""`) specialization стала успешной (`202`, `user code loaded`),
|
||
но запросы всё равно зависали.
|
||
5. Прямая проверка контейнера показала второй контрактный нюанс node runtime:
|
||
- пользовательская функция должна возвращать объект формата
|
||
`{ status, body, headers? }`
|
||
- возврат простой строки приводил к зависанию ответа (callback не отправлял HTTP response).
|
||
|
||
### Принятые изменения
|
||
|
||
- `nodejs-acc` закреплен на `ghcr.io/fission/node-env:1.32.5`.
|
||
- Для JS функций (`fn-js-acc`, `fn-js-direct`, тестовые `jsm1..jsm4`):
|
||
- выставлен пустой entrypoint (`functionName=""`)
|
||
- код приведен к контракту Node runtime:
|
||
- `module.exports = async function(context) { return { status: 200, body: "..." }; }`
|
||
- Для основных JS trigger включены методы `GET` и `POST`.
|
||
|
||
### Проверка
|
||
|
||
- UI invoke:
|
||
- `fn-js-acc` -> `status=200`, `response_raw=hello-js-ok`
|
||
- `fn-js-direct` -> `status=200`, `response_raw=hello-js-ok`
|
||
- Прямые GET через роутер:
|
||
- `/js-acc` -> 200
|
||
- `/js-direct` -> 200
|
||
- Тестовые `jsm1..jsm4` после унификации кода -> 200 через UI invoke.
|
||
|
||
### Вывод
|
||
|
||
Проблема была не в UI-рендеринге и не в одном конкретном trigger, а в несовпадении контракта Node runtime:
|
||
- entrypoint/path specialization
|
||
- shape возвращаемого значения функции.
|
||
|
||
---
|
||
|
||
## Сессия: Финальный fix `fn-go-acc` (Go runtime specialization)
|
||
|
||
### Симптом
|
||
|
||
- `fn-go-acc` выдавал timeout на invoke через console/router.
|
||
|
||
### Диагностика
|
||
|
||
1. На исходной конфигурации в executor логах:
|
||
- `plugin.Open("/userfunc/deployarchive/main.go"): invalid ELF header`.
|
||
2. Это означало, что runtime ожидал plugin-артефакт, но в package deployment лежал исходник `main.go`.
|
||
3. После первой попытки собрать plugin локальным `go1.26.1` ошибка `invalid ELF` ушла, но появился новый фейл specialization:
|
||
- `Post "http://127.0.0.1:8888/v2/specialize": EOF`.
|
||
4. Вывод: plugin собран несовместимой версией Go относительно runtime.
|
||
|
||
### Решение
|
||
|
||
- Сборка plugin выполнена в `ghcr.io/fission/go-builder` (Go 1.25.6), то есть тем же toolchain, что у Fission environment builder:
|
||
- `GO111MODULE=off /usr/local/go/bin/go build -buildmode=plugin -o main.so ./main.go`
|
||
- упаковка `main.so` в `deploy.zip`.
|
||
- Обновление функции через CLI:
|
||
- `fission function update -n default --name fn-go-acc --env go-acc --entrypoint Handler --deployarchive /tmp/fn-go-acc-fix2/deploy.zip -f`
|
||
|
||
### Проверка
|
||
|
||
- `/go-acc` через router с JWT: `hello-go-ok`, `HTTP 200`.
|
||
- В executor логах specialization успешен:
|
||
- `specialized pod`
|
||
- `added function service`
|
||
- без `invalid ELF` и без `EOF`.
|
||
|
||
### Контрольная матрица маршрутов
|
||
|
||
- `/auto/ok` -> 200
|
||
- `/js-acc` -> 200
|
||
- `/js-direct` -> 200
|
||
- `/go-acc` -> 200
|
||
|
||
### Практический вывод
|
||
|
||
Для go-env в этом кластере критично соблюдать контракт runtime:
|
||
1. deploy-пакет должен содержать именно plugin (`.so`), а не исходник.
|
||
2. plugin должен быть собран совместимой версией Go (через `ghcr.io/fission/go-builder`).
|
||
|
||
---
|
||
|
||
## Сессия: `Edit Code` для `fn-go-acc` пустой
|
||
|
||
### Симптом
|
||
|
||
- В UI (`GET /console/api/functions/fn-go-acc`) поле `code` было пустым.
|
||
|
||
### Почему так произошло
|
||
|
||
- После фикса runtime функция была переведена на deploy-архив plugin (`main.so`).
|
||
- Package имел вид:
|
||
- `spec.deployment.type=url`
|
||
- `spec.deployment.url=...`
|
||
- без `spec.deployment.literal`
|
||
- Backend в `handleGetFunction` извлекал код только из `spec.deployment.literal`, поэтому для URL-пакета отдавал пустую строку.
|
||
|
||
### Что изменено
|
||
|
||
- В backend добавлен `extractPackageSourceCode(...)` с fallback-приоритетом:
|
||
1. `spec.source.literal`
|
||
2. `spec.deployment.literal`
|
||
3. `spec.source.url`
|
||
4. `spec.deployment.url`
|
||
- Добавлена загрузка URL-архива через `fetchPackageArchive(...)` и декодирование через `decodeArchiveBytesToSource(...)`.
|
||
- Обновлен unit-тестами сценарий, где `deployment.literal` отсутствует, но есть `source.literal`.
|
||
|
||
### Операционные шаги
|
||
|
||
- Собран и задеплоен `naeel/fission-console:v0.2.6`.
|
||
- Для текущей `fn-go-acc` дополнительно обновлен `sourcearchive` (`main.go`), чтобы исходник гарантированно отображался в Edit Code.
|
||
|
||
### Результат
|
||
|
||
- `GET /console/api/functions/fn-go-acc` теперь возвращает непустой Go source в `code`.
|
||
- Вызов `/go-acc` остается рабочим (`hello-go-ok`, HTTP 200).
|
||
|
||
---
|
||
|
||
## Сессия: Проверка TLS + branding NUBES + верификация `tf-neg-syntax-fn`
|
||
|
||
### TLS
|
||
|
||
- Проверен доступ к console:
|
||
- `http://.../console/` -> `308` redirect на HTTPS
|
||
- `https://.../console/` -> `200`
|
||
- Ingress конфиг содержит корректный TLS блок и принудительный SSL redirect.
|
||
- Вывод: endpoint защищен TLS, «незащищенного HTTP-доступа» нет.
|
||
|
||
### Брендинг NUBES
|
||
|
||
- В `console/ui/index.html` добавлены:
|
||
- `<title>NUBES Fission Console</title>`
|
||
- favicon (inline SVG)
|
||
- navbar brand block с `NUBES` / `FISSION CONSOLE`.
|
||
- Обновлен образ до `naeel/fission-console:v0.2.9`.
|
||
|
||
### `tf-neg-syntax-fn` — почему timeout вместо прямого SyntaxError
|
||
|
||
- Функция содержит заведомо битый Python (`def main(:`), confirmed через `GET /console/api/functions/tf-neg-syntax-fn`.
|
||
- На runtime это роняет specialization (`500`), после чего executor ретраит.
|
||
- Поэтому router/console не получает «чистый traceback» сразу из runtime API и видит timeout/ошибку specialization.
|
||
- Для UX введен fail-fast и явная ошибка в console invoke:
|
||
- `timeout after 20s: function specialization likely failed (for example, syntax error)`.
|
||
|
||
---
|
||
|
||
## Сессия: "Не защищено красным" — экстренное лечение
|
||
|
||
### Наблюдение
|
||
|
||
- Сертификат endpoint сам по себе валидный (Let's Encrypt, CN/SAN = `fission.kube5s.ru`).
|
||
- Но на том же host были два ingress с разной TLS-конфигурацией:
|
||
- `fission-console` с TLS
|
||
- `fission-router` без TLS
|
||
|
||
### Действие
|
||
|
||
- Патч `fission-router`:
|
||
- добавлен `spec.tls` с `secretName: fission-tls`
|
||
- добавлен `force-ssl-redirect=true`
|
||
|
||
### Результат
|
||
|
||
- HTTP на роутер-пути дает 308 -> HTTPS
|
||
- HTTPS на console стабильно 200
|
||
- Конфигурация host выровнена: теперь и console, и router на одном сертификате.
|
||
|
||
---
|
||
|
||
## Сессия: Строгая чистка до "только точно рабочие и видимые"
|
||
|
||
### Запрос
|
||
|
||
- Оставить только функции, которые в UI/Console:
|
||
- показывают исходник (`code` не пустой)
|
||
- реально вызываются (`invoke.status == 200`)
|
||
|
||
### Что произошло
|
||
|
||
- Выполнен массовый cleanup с keep-list.
|
||
- По логу cleanup выявилась ошибка jsonpath в пост-обработке orphan-пакетов:
|
||
- использовалось `{n}` вместо `{"\\n"}`
|
||
- это ломало шаг удаления orphan-объектов после основного удаления.
|
||
- Основное удаление функций при этом сработало корректно.
|
||
|
||
### Проверка после cleanup
|
||
|
||
- Текущее состояние:
|
||
- functions: 6
|
||
- httptriggers: 6
|
||
- packages: 6
|
||
- Остались только:
|
||
- `auth-test2`
|
||
- `fn-js-direct`
|
||
- `fn-js-acc`
|
||
- `hello`
|
||
- `tf-auto-ok-fn`
|
||
- `tf-stress-fast-fn`
|
||
- Для каждой функции через Console API подтверждено:
|
||
- `code=OK`
|
||
- `invoke=OK`
|
||
|
||
### Вывод
|
||
|
||
- Финальный набор теперь соответствует строгому критерию "однозначно работает и показывается".
|
||
|
||
---
|
||
|
||
## Сессия: Возврат к 15 функциям + проверка ожиданий + live favicon
|
||
|
||
### Наблюдение в начале
|
||
|
||
- После строгой чистки в кластере оставалось 6 функций.
|
||
- В live UI favicon оставался старым, хотя в репозитории правка уже была.
|
||
- Проверка deployment показала: работал старый image `naeel/fission-console:v0.3.0`.
|
||
|
||
### Расширение набора до 15
|
||
|
||
- Добавлены функции из примеров:
|
||
- `tf-hello-fn`, `tf-deep-recursion-fn`, `tf-destroy-test-fn`, `tf-freq-update-fn`, `tf-orphan-fn`
|
||
- `tf-multi-env-1-fn`, `tf-multi-env-2-fn`, `tf-multi-env-3-fn`
|
||
- Добавлен негативный кейс:
|
||
- `tf-neg-syntax-fn` + route `/neg/syntax`
|
||
- Итоговый размер:
|
||
- functions = 15
|
||
- httptriggers = 15
|
||
|
||
### Что сломалось и как починено
|
||
|
||
1. `multi-env-1/2/3` не применялись:
|
||
- ошибка Terraform: `Invalid escape sequence` из-за `"$\{path.module\}/code"`.
|
||
- фикс: заменено на `"${path.module}/code"` во всех трех `main.tf`.
|
||
|
||
2. После применения `multi-env-1/2/3` маршруты таймаутили (`rc=28`):
|
||
- причина: image env `ghcr.io/fission/python-env:v1.20.0`.
|
||
- фикс: переключено на `ghcr.io/fission/python-env`, re-apply всех трех модулей.
|
||
- результат: `/multi-env-1`, `/multi-env-2`, `/multi-env-3` -> `HTTP 200`.
|
||
|
||
### Финальная тест-матрица
|
||
|
||
- Router tests с JWT: `PASS=15, FAIL=0`.
|
||
- Успешные 200: базовые + multi-env + tf-hello + stress-fast.
|
||
- Ожидаемые ошибки:
|
||
- `/deep-recursion` -> timeout (`HTTP 000`, `rc=28`)
|
||
- `/neg/syntax` -> timeout (`HTTP 000`, `rc=28`)
|
||
- Console invoke подтверждает:
|
||
- positive функции -> `status=200`
|
||
- negative функции -> fail-fast timeout error.
|
||
|
||
### Favicon в live
|
||
|
||
- Собран и выкачен `naeel/fission-console:v0.3.1`.
|
||
- `deployment/fission-console` обновлен и rollout успешен.
|
||
- Проверка `https://fission.kube5s.ru/console/` показывает нужный favicon URL:
|
||
- `https://nubes.ru/themes/custom/nubes_2025/favicon.png`.
|