20 KiB
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— из секретаrouterJWT_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:
- Добавил поля в server struct:
authUser,authPass,tokenMu sync.Mutex,cachedJWT,tokenExpAt - Новый метод
getRouterToken():- Если
authUser/authPassзаданы → делает POST/auth/login - Кеширует JWT на 100 сек (JWT expiry = 120 сек, берём с запасом)
- Fallback на SA token если login не удался
- Если
- В
handleInvokeFunctionвызываетgetRouterToken()и ставитAuthorization: Bearer <token> - 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"} - На другие пути проверяет
Authorizationheader - Создаёт функцию через 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 окружения могла проходить, но ответ функции не возвращался.
Ключевые находки
- Node runtime работал через
POST /v2/specialize. - При
functionName=mainruntime строил путь загрузки как: -/userfunc/deployarchive/main - В реальном поде
/userfunc/deployarchiveбыл файлом (literal payload), а не директорией. - После перевода на пустой entrypoint (
functionName="") specialization стала успешной (202,user code loaded), но запросы всё равно зависали. - Прямая проверка контейнера показала второй контрактный нюанс 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.
Диагностика
- На исходной конфигурации в executor логах:
-
plugin.Open("/userfunc/deployarchive/main.go"): invalid ELF header. - Это означало, что runtime ожидал plugin-артефакт, но в package deployment лежал исходник
main.go. - После первой попытки собрать plugin локальным
go1.26.1ошибкаinvalid ELFушла, но появился новый фейл specialization: -Post "http://127.0.0.1:8888/v2/specialize": EOF. - Вывод: 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:
- deploy-пакет должен содержать именно plugin (
.so), а не исходник. - 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.literal2.spec.deployment.literal3.spec.source.url4.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/->308redirect на 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
Что сломалось и как починено
-
multi-env-1/2/3не применялись: - ошибка Terraform:Invalid escape sequenceиз-за"$\{path.module\}/code". - фикс: заменено на"${path.module}/code"во всех трехmain.tf. -
После применения
multi-env-1/2/3маршруты таймаутили (rc=28): - причина: image envghcr.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.