# 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 ` Результат: `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 ` 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 ↓ 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` добавлены: - `NUBES Fission Console` - 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`.