Files

421 lines
20 KiB
Markdown
Raw Permalink 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.
# 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`.