- Определена архитектура managed IoT service - Согласовано решение: RabbitMQ (MVP), потом Kafka; EMQX + CRD контроллер - Создан подробный план для Sonnet (doc/iot-mvp-plan.md) - Добавлено правило в copilot-instructions: лог мышления в doc/thinking/ по датам - Полный ход рассуждений в doc/thinking/2026-04-04.md Ключевые решения: - IoT код в iot/ (легко вынести потом) - CRD IoTDevice + контроллер (как все остальное в sless) - EMQX HTTP Auth Backend для динамической аутентификации устройств - Архитектура broker-agnostic (легко переключить на Kafka) - Terraform: расширяем текущий provider (sless_iot_device ресурс)
82 KiB
Ошибки и решения
Сюда записываем проблемы с которыми столкнулись и как их решили.
2026-04-01 — Nubes PostgreSQL API: ошибки при создании ресурсов (PG_TEST)
Все ошибки воспроизводились в examples/PG_TEST при тестировании провайдера
terra.k8c.ru/nubes/nubes v5.0.51. VM: naeel@5.172.178.213, PG-инстанс: pg-test-02.
ERR-PG-01: "Invalid JSON String" при создании инстанса с json_parameters
Симптом
Error: Ошибка клиента
with nubes_postgres.pg_test_instance
Invalid JSON String
Причина
При передаче параметра json_parameters (строка JSON с кастомными настройками PG)
Nubes API v5 возвращает "Invalid JSON String" независимо от корректности самого JSON.
Вероятно — баг в провайдере или несовместимость формата с deck-api-test.
Решение
Убран json_parameters из конфигурации nubes_postgres. PG запускается
с дефолтными параметрами движка. При необходимости custom params — требует
диагностики на стороне Nubes (.api_endpoint = deck-api-test.ngcloud.ru).
Файл: examples/PG_TEST/postgres.tf
ERR-PG-02: vault_secrets меняется вне Terraform → destroy+recreate всей цепочки
Симптом
Каждый terraform apply обнаруживает изменения "снаружи Terraform":
Note: Objects have changed outside of Terraform
# nubes_postgres.pg_test_instance has changed
~ vault_secrets = (sensitive value)
Это вызывает план с -/+ destroy and then create replacement для pg_test_user
и pg_test_db, хотя реально они не изменились.
Причина
Nubes API обновляет vault_secrets (путь к Vault с паролями пользователей)
каждый раз при создании/удалении пользователей. Terraform видит это как
"изменение снаружи" и считает postgres_id изменённым (т.к. он (known after apply)
после обновления инстанса), что форсирует замену всех дочерних ресурсов.
Попытка решения
Добавить lifecycle { ignore_changes = [vault_secrets] } — не работает.
Terraform выводит предупреждение:
"The attribute vault_secrets is decided by the provider alone and therefore there can be no configured value to compare with. Including this attribute in ignore_changes has no effect."
vault_secrets — Computed-only (провайдер его полностью контролирует),
ignore_changes для таких атрибутов игнорируется.
Реальная причина destroy+recreate: при обнаружении vault_secrets как
"изменённого снаружи" Terraform обновляет nubes_postgres in-place, но
в плане ставит id = (known after apply) — это форсирует замену зависимых
ресурсов у которых postgres_id ссылается на nubes_postgres.*.id.
Статус: открытая проблема. Обходной путь — выполнять apply только на
чистом state (без накопленных изменений снаружи). После первого полного
apply с lifecycle ignore_changes убран как неэффективный.
Файл: examples/PG_TEST/postgres.tf
ERR-PG-03: Race condition при параллельном создании пользователей
Симптом
При одновременном создании двух и более nubes_postgres_user на одном инстансе:
Error: Ошибка клиента
with nubes_postgres_user.test_extra_user1
операция XXXX завершилась с ошибкой: Секрет для пользователя extra_user1 не был создан
Или:
операция XXXX завершилась с ошибкой: key doesn't exist
Причина
Nubes API не поддерживает параллельное создание пользователей на одном PG-инстансе. Внутри Nubes: каждое создание пользователя пишет в Vault, а Vault/Deck не справляются с конкурентными записями в один Secret.
Решение
Принудительная последовательная цепочка через depends_on:
pg_test_user → pg_test_db → extra_user1 → extra_user2 → extra_db1 → extra_db2
Каждый nubes_postgres_user и nubes_postgres_database явно ждёт предыдущий.
depends_on нужен даже если прямой ссылки на атрибуты нет.
Файлы: examples/PG_TEST/postgres.tf, examples/PG_TEST/postgres_extra.tf
ERR-PG-04: Роль app_user не работает для nubes_postgres_user
Симптом
Error: Ошибка клиента
with nubes_postgres_user.test_app_user,
операция XXXX завершилась с ошибкой: Секрет для пользователя test_app_user не был создан
Ресурс висит ~3 минуты перед ошибкой. Воспроизводится стабильно.
Причина
Роль app_user не поддерживается для создания PostgreSQL пользователей
через nubes_postgres_user в данной версии провайдера/API. Возможно, роль
предусмотрена только для другого механизма доступа.
Решение
Использовать только роль ddl_user для ресурса nubes_postgres_user.
Создание пользователей с app_user — не работает на deck-api-test.ngcloud.ru.
Из конфигурации удалён ресурс nubes_postgres_user.test_app_user.
ERR-PG-05: "Нарушена консистентность" — пользователь есть в API, нет в state_out
Симптом
Error: Нарушена консистентность
with nubes_postgres_user.test_extra_user1
Операция вернула duplicate/exist, но объект не найден в state_out
Причина
Пользователь extra_user1 был создан Nubes API на предыдущей (упавшей) попытке apply.
Terraform state не зафиксировал успех (т.к. apply завершился ошибкой), но Nubes
счётной записью extra_user1 не удалил.
Флаг adopt_existing_on_create = true должен был решить это, но он проверяет
state_out инстанса — а там пользователь не отражается (из-за ERR-PG-02:
vault_secrets изменялся и инстанс был в "changed outside" состоянии).
Решение
Полный terraform destroy для очистки state + ресурсов в API, затем
terraform apply с уже включённым lifecycle { ignore_changes = [vault_secrets] }.
После этого проблема не воспроизводится.
ERR-PG-06: Второй пользователь на инстансе не может получить vault_secrets
Симптом
При создании второго пользователя на PG-инстансе (первый уже существует):
Error: Ошибка клиента
with nubes_postgres_user.test_extra_user1
операция 4D816F17-F43E-4062-AE37-5098ADE07041 завершилась с ошибкой:
Секрет для пользователя test_eu1 не был создан
Ресурс висит ~3–4 минуты, затем падает с этой ошибкой. Воспроизводится стабильно
для любого второго пользователя (проверено на extra_user1, test_eu1)
независимо от роли (ddl_user), имени и порядка depends_on.
Важно: pg_test_user (первый пользователь, созданный при инициализации
инстанса) всегда успешно проходит через adoption за 1 секунду — его vault_secret
уже был создан при первом apply. Только создание нового второго пользователя
всегда приводит к этой ошибке.
Причина
Vault backend для PG-инстанса e0e74801 (тест-окружение k8s-3-sandbox-nubes-ru)
вероятно ограничен одной vault-записью на инстанс. При попытке создать vault_secrets
для второго пользователя — запись не создаётся, провайдер возвращает ошибку через
~3–4 минуты ожидания.
Либо vault policy для данного инстанса предусмотрена только для основного
пользователя (user0). Дополнительные пользователи не имеют прав vault-path.
Статус: открытая проблема, требует диагностики на стороне Nubes.
Следствие для архитектуры: В текущем тест-окружении Nubes PostgreSQL поддерживает только одного пользователя с vault_secrets на инстанс. Lifecycle-тесты с несколькими пользователями (goal текущей сессии) — невозможны без исправления vault-конфигурации.
Файл: examples/PG_TEST/postgres_extra.tf
ERR-PG-07: HTTP 408 от IAM API (auth-api-test.ngcloud.ru)
Симптом
Error: Ошибка клиента
with nubes_postgres_user.pg_test_user
ошибка API 408: {"IAM URL":"https://auth-api-test.ngcloud.ru/api/v1/auth/user",
"idpResponse":{"prefix":{"status_text":"Request Time-out","statuscode":"408 Request Time-out"}}}
Может проявляться даже на этапе terraform plan (refresh инстанса).
Причина
Транзитная перегрузка тест-IAM-сервиса auth-api-test.ngcloud.ru. Возникает
после серии интенсивных apply/destroy в течение одного или нескольких часов.
Решение
Подождать 5–10 минут и повторить apply. Ошибка проходит самостоятельно.
Важно: в production окружении (auth-api.ngcloud.ru) эта проблема
предположительно не воспроизводится.
2026-04-03 — Коррекции и новые находки (PostgreSQL续)
ERR-PG-02: id=(known after apply) при Update — ИСПРАВЛЕНО ✅
Статус обновления: Проблема уже решена в текущей версии провайдера.
Где было: internal/provider/postgres_resource.go Update() метод устанавливал id = (known after apply)
Как исправлено: Строка ~845 добавлена явная сохранение ID:
// fix: plan.ID is Computed (empty in plan), must preserve existing ID from state
plan.ID = state.ID
Проверка: terraform plan больше НЕ показывает destroy+recreate зависимых ресурсов.
Результат плана (ответно): Plan: 0 to add, 1 to change, 0 to destroy (только update, без replace)
Вывод: ERR-PG-02 не актуален для текущей версии провайдера.
ERR-PG-06: Второй пользователь — ПЕРЕКВАЛИФИЦИРОВАНО 🔄
Изменение статуса: ERR-PG-06 была НЕВЕРНО диагностирована.
Что было думано: "Vault backend ограничен одним пользователем на инстанс"
Что обнаружено: pg_test_user3 (u3) успешно создан в terraform state! Это второй пользователь на инстансе pg-test-02.
Проверки:
terraform state list
# Output:
# nubes_postgres.pg_test_instance
# nubes_postgres_user.pg_test_user ← user0
# nubes_postgres_user.pg_test_user3 ← u3 (УСПЕШНО)
Статус: Многопользовательское создание РАБОТАЕТ при правильной последовательности depends_on.
Реальная проблема: Не в создании пользователей, а в том что файл postgres_extra.tf с третьим пользователем был переименован в postgres_extra.tf11 (бэкап), и текущий конфиг не имел этого файла.
Вывод: ERR-PG-06 была следствием неполного тестирования, а не действительным ограничением API.
ERR-PG-08: Concurrent Operations Are Not Supported ❌ НОВАЯ ПРОБЛЕМА
Симптом
После успешного завершения Update операции на nubes_postgres, попытка создать
зависимый ресурс (например БД) немедленно падает с ошибкой 422:
Error: Ошибка клиента
with nubes_postgres_database.pg_test_db
ошибка API 422: {
"DETAIL": "There is a started operation on this instance",
"TYPE": "about:blank",
"TITLE": "Concurrent operations are not supported (job status: SUCCESS)"
}
Когда воспроизводится
terraform planобнаруживает изменения (например,vault_secretsdrift)- Apply запускает Update инстанса:
nubes_postgres.pg_test_instance: Modifying... - Update успешно завершается:
Modifications complete after 0s - Terraform пытается создать DB:
nubes_postgres_database.pg_test_db: Creating... - FAIL: 422 Concurrent operations
Попытки обхода (неуспешные)
- Добавлен
depends_on = [pg_test_user3]— не помогло ✗ - Ожидание 60 секунд между apply'ами — не помогло ✗
- Ожидание 120 секунд — не помогло ✗
Причина
Nubes API имеет встроенный serial-lock на операции per-instance. Даже хотя
WaitForOperation() возвращает IsSuccessful = true, сервер всё ещё обрабатывает
асинхронные побочные эффекты (Vault sync, state reconciliation и тд). Новые запросы
на операции отклоняются с 422 до полного завершения.
Текущий workaround
Разбить apply на несколько фаз вручную:
# Фаза 1: создание инстанса + пользователей (без БД)
cp postgres.tf postgres.tf.bak
sed -i '/resource.*nubes_postgres_database/,/^}/d' postgres.tf
terraform apply -auto-approve
# Фаза 2: добавить БД обратно и применить
cp postgres.tf.bak postgres.tf
terraform apply -auto-approve
Статус: Открытая проблема в провайдере. Требует fix на уровне WaitForOperation().
Рекомендуемое решение: Добавить post-completion delay (30–60 сек) или retry mechanism
в internal/provider/client_impl.go метод WaitForOperation.
Файл для анализа: internal/provider/client_impl.go line 240+
Документация: ERR-PG-08-concurrent-operations.md
2026-03-29 — SSH timeout после destroy VM example
Симптом
После terraform destroy для examples/VM попытка зайти по SSH на target VM завершилась таймаутом:
ssh: connect to host 185.247.187.154 port 22: Connection timed out
Причина
Это ожидаемое поведение для сценария suspend: VM перестаёт отвечать по SSH после destroy, а затем поднимается обратно на terraform apply.
Что сделали
- Подтвердили, что
terraform applyпосле destroy восстанавливает доступ и повторно запускает install jobs.
2026-03-26 — БАГ ГЕНЕРАТОРА: modify-only поля помечаются Required в schema ресурса
Error: Missing required argument
on vc_org.tf line 5, in resource "nubes_vc_org" "dev_org":
5: resource "nubes_vc_org" "dev_org" {
The argument "v_i_p_configure" is required, but no definition was found.
The argument "resource_name" is required, but no definition was found.
Причина
Генератор (~/terra/terraform/devops/) при создании Go-кода ресурса (19_vc_org_resource.go)
помечает все операции ресурса как Required в схеме, включая поля,
которые нужны только для операции modify (не для create).
Конкретный пример: vIPConfigure (код параметра 662) — это поле операции modify,
но попадает в schema с Required: true:
"v_i_p_configure": schema.StringAttribute{Required: true},
В YAML-описании сервиса (19_vc_org.yaml) vIPConfigure объявлен только под
operations.modify.params, а не под operations.create.params.
Что нужно исправить в генераторе
В ~/terra/terraform/devops/ (файлы 02_generate_resources_and_docs*.go/sh):
Поля, принадлежащие только операции modify (или другим не-create операциям),
должны генерироваться как Optional: true, Computed: true, а не Required: true.
Логика:
- поле в
operations[create].params→Required: true - поле только в
operations[modify].params→Optional: true, Computed: true - поле только в state (read-only) →
Computed: true
Временный workaround (действующий)
В terraform.tf-манифесте указывать пустую строку:
v_i_p_configure = "" # modify-only поле; при create не отправляется в API
Провайдер при Create не передаёт это поле в API (строка 418/556 params), но schema.Required требует non-null значение в плане.
Файлы для правки
~/terra/terraform/devops/profiles/test/generated/go/19_vc_org_resource.go— сгенерированный, не менять вручную- Править нужно шаблоны/генераторы в
~/terra/terraform/devops/
2026-03-22 — БАГ: CreateService/CreateFunction возвращает 409 при terraform apply -replace (ИСПРАВЛЕН)
Симптом
terraform apply -replace=sless_service.pg_info
sless_service.pg_info: Destroying... [name=pg-info]
sless_service.pg_info: Destruction complete
sless_service.pg_info: Creating...
Error: create service: status 409: {"error":"service already exists"}
Все 22+ сервиса из -replace падают с 409 при пересоздании.
Точная причина
Цепочка событий (воспроизводится только при наличии finalizer):
terraformвызываетDELETE /services/pg-info- API делает
h.K8s.Delete(svc)→ k8s НЕ удаляет объект немедленно.
Вместо этого — выставляетDeletionTimestampна объекте и ждёт. service_controller.go(асинхронно!) обрабатывает удаление:- сносит Deployment, k8s Service, Ingress
- затем вызывает
svc.Finalizers = removeString(svc.Finalizers, serviceFinalizerName) - только после этого k8s реально удаляет CRD объект из etcd
- латентность: 1–5 секунд
terraformнемедленно вызываетPOST /services/pg-info- API делает
h.K8s.Create(svc)→ etcd возвращаетIsAlreadyExists - Старый код проверял:
phase == Failed? → нет, былоReady→shouldRecreate=false→ 409
Ключевое: объект существует с DeletionTimestamp != zero, то есть он уже "мёртвый", но finalizer ещё не снят. Старый код этого не проверял.
Файл с багом
internal/api/handler/services.go → CreateService() — строка shouldRecreate
internal/api/handler/functions.go → CreateFunction() — аналогичная логика
Исправление
В обоих файлах добавлена проверка !existing.DeletionTimestamp.IsZero() до проверки shouldRecreate:
if getErr == nil && !existing.DeletionTimestamp.IsZero() {
// Объект удаляется: polling каждую секунду до 30 сек
for i := 0; i < 30; i++ {
time.Sleep(1 * time.Second)
if errors.IsNotFound(h.K8s.Get(...)) {
// исчez — создаём
}
}
// таймаут — возвращаем 409 с "try again later"
}
Почему именно polling, а не Watch
http.ResponseWriter не поддерживает long-poll без дополнительного механизма.
Watch на объект внутри HTTP handler — антипаттерн (использует goroutine leak при отмене).
30 × 1s — достаточно для любого разумного кластера; terraform имеет свой retry.
2026-03-22 — ПОВЕДЕНИЕ: оператор выставляет Ready до готовности pod (known limitation)
Симптом
GET /v1/namespaces/sless-xxx/services/my-svc → { phase: "Ready" }
POST /fn/sless-xxx/my-svc → HTTP 502
# Через 15-30 секунд — 200 OK
Причина
service_controller.go выставляет phase=Ready когда Deployment создан в k8s
(успешный client.Create / client.Update), но не дожидается пока pod пройдёт
readiness check и начнёт принимать трафик.
Pod startup (скачать образ + старт nodejs/python) занимает 5-30 секунд после Deployment.
Это не баг — known limitation
Kubernetes Deployment не даёт синхронного ответа о готовности pod.
Оператор выставляет Ready через RequeueAfter: 60s, ожидание pod-ready потребует
Watch на Deployment.Status.ReadyReplicas — усложняет reconcile loop.
Правило для тестов
Все invoke-тесты обязаны включать:
sleep 15-30послеwait_phase(..., "Ready")- retry (≥3-5 попыток с интервалом 15с) перед
[FAIL]
Обнаружено
G17 (nodejs20), G18 (env_vars), G22D (invoke isolation), G20C (multi-svc load).
2026-03-21 — БАГ: memory_mb через PUT не применяется в k8s Deployment (ИСПРАВЛЕН v0.1.49)
Симптом
PUT /v1/namespaces/sless-xxx/services/my-svc
{ "memory_mb": 256, ... }
→ HTTP 200 OK
kubectl get deployment my-svc -n sless-fn-xxx
containers[0].resources.limits.memory: 128Mi ← было 128, осталось 128
Причина
controllers/service_controller.go:ensureServiceDeployment при UPDATE существующего Deployment обновляет только два поля:
existing.Spec.Template.Spec.Containers[0].Image = svc.Status.ImageRef
existing.Spec.Template.Spec.Containers[0].Env = desired.Spec.Template.Spec.Containers[0].Env
// Resources (memory limit) НЕ обновляется!
desired Deployment строится с правильным memory_mb, но в existing он не копируется.
Фикс (применён в v0.1.49)
// В ensureServiceDeployment, блок else (UPDATE):
existing.Spec.Template.Spec.Containers[0].Image = svc.Status.ImageRef
existing.Spec.Template.Spec.Containers[0].Env = desired.Spec.Template.Spec.Containers[0].Env
existing.Spec.Template.Spec.Containers[0].Resources = desired.Spec.Template.Spec.Containers[0].Resources // ← ДОБАВЛЕНО
existing.Spec.Template.Spec.ImagePullSecrets = desired.Spec.Template.Spec.ImagePullSecrets
Проверка
operator_lifecycle_test.sh тест 2.13 → PASS (47/47)
Файл
controllers/service_controller.go ~ строка 241
Обнаружен
operator_lifecycle_test.sh, тест 2.13
2026-03-21 — БАГ: нет self-healing — ручное удаление Deployment не восстанавливается (ИСПРАВЛЕН v0.1.49)
Симптом
kubectl delete deployment my-svc -n sless-fn-sless-xxx
# Deployment исчез.
# Ждём 90s — контроллер не пересоздаёт.
# GET /fn/sless-xxx/my-svc → 502 (pod отсутствует, CRD=Ready)
Причина
ServiceReconciler реагирует только на изменения объектов типа Service (sless CRD) в namespace sless.
Deployment живёт в sless-fn-sless-xxx — другой namespace. controller-runtime не допускает Owns() для cross-namespace ресурсов.
ensureServiceDeployment вызывается только когда phase=Ready И пришёл reconcile event (=изменение CRD). Просто удалённый Deployment event не генерирует.
Фикс (применён в v0.1.49)
Добавлен RequeueAfter: 60s в конец ensureServiceDeployment:
// Периодический requeue — self-healing: если Deployment/Service/Ingress удалены вручную, контроллер их пересоздаст
return ctrl.Result{RequeueAfter: 60 * time.Second}, nil
Проверка
operator_lifecycle_test.sh тест 3.2 → PASS (47/47)
Файл
controllers/service_controller.go ~ строка 340
Обнаружен
operator_lifecycle_test.sh, тест 3.2
2026-03-21 — Баг: DELETE несуществующего ресурса → HTTP 204 вместо 404
Симптом
DELETE /v1/namespaces/sless-xxx/functions/not-exists
→ HTTP 204 (пустой ответ, как будто удаление прошло успешно)
Аналогично для services, triggers, jobs.
Причина
Все четыре Delete* хендлера при errors.IsNotFound(err) выполняли w.WriteHeader(http.StatusNoContent) вместо возврата 404.
// БЫЛО (ошибочно):
if errors.IsNotFound(err) {
w.WriteHeader(http.StatusNoContent)
return
}
// СТАЛО (правильно):
if errors.IsNotFound(err) {
writeJSON(w, http.StatusNotFound, errResp("function not found"))
return
}
Затронутые файлы
internal/api/handler/functions.go—DeleteFunctioninternal/api/handler/services.go—DeleteServiceinternal/api/handler/triggers.go—DeleteTriggerinternal/api/handler/jobs.go—DeleteJob
Фикс
Коммит e8d0d78, оператор v0.1.44.
2026-03-21 — Баг: funcs-service pod в ImagePullBackOff после обновления образа
Симптом
После kubectl set image deployment/sless-funcs-service funcs=pearlharbor…/sless-funcs-service:v0.2.1:
Events:
Warning Failed kubelet Failed to pull image "...v0.2.1":
authorization failed: no basic auth credentials
Новый под застрял в ImagePullBackOff. Старый под продолжал работать с v0.2.0 (без services).
Причина
deployments/k8s/funcs-service.yaml не содержал imagePullSecrets, а образ находится в приватном реестре Harbor (pearlharbor.registryk8s.services.ngcloud.ru).
Старый под (v0.2.0) работал с другой нодой, где уже был cached образ с credentials.
Фикс
spec:
imagePullSecrets:
- name: sless-registry-auth
containers:
- name: funcs
image: pearlharbor.registryk8s.services.ngcloud.ru/naeel/sless-funcs-service:v0.2.2
Коммит 09b3588. Правило: все образы из pearlharbor требуют imagePullSecrets: sless-registry-auth.
2026-03-21 — Баг: /funcs/{ns}/source/{fn} → 404 для sless_service ресурсов
Симптом
Клик на карточку pg-info (sless_service) в web-консоли → ошибка 404 при загрузке кода.
GET /funcs/sless-ffd1f598c169b0ae/source/pg-info → HTTP 404
{"error":"function not found"}
Причина
proxySourceGet в funcs-service всегда обращался к оператору по пути:
/v1/namespaces/{ns}/functions/{fn}/source
pg-info — это sless_service (Kind=Service), а не sless_function. У оператора не было эндпоинта /services/{name}/source.
Фикс
- Оператор (
internal/api/handler/source.go): добавленGetServiceSource— делает то же чтоGetSource, но читаетServiceCRD вместоFunction. - Оператор (
internal/api/router.go): зарегистрирован маршрутGET /v1/namespaces/{ns}/services/{name}/source. - funcs-service (
services/funcs/main.go):proxySourceGetпринимает параметрkind; приkind=serviceобращается к/services/…/source. - funcs-service (
services/funcs/index.html):loadSource(body, ns, fnName, fnKind)добавляет?kind=serviceв fetch URL для сервисов.
Коммит 50f2456, оператор v0.1.45, funcs-service v0.2.2.
2026-03-20 — Баг: неверный hostname в api_endpoint провайдера nubes
Симптом
terraform apply на examples/POSTGRES падал с HTTP 404 на обоих ресурсах: nubes_postgres.npg и nubes_s3bucket.baba_bucket.
Причина
В examples/POSTGRES/main.tf был указан UI-домен вместо API-домена:
# НЕПРАВИЛЬНО (UI облака, не API):
api_endpoint = "https://deck-test.ngcloud.ru/api/v1"
# ПРАВИЛЬНО (API Dashboard):
api_endpoint = "https://deck-api-test.ngcloud.ru/api/v1/index.cfm"
Аналогично для nubes_endpoint в провайдере sless.
Фикс
deck-test → deck-api-test в обоих провайдерах, добавлен /index.cfm.
Найдено через git show cca3a8c:examples/POSTGRES/main.tf.
Правило
deck-api-test.ngcloud.ru— API (для Terraform)
deck-test.ngcloud.ru— UI облака (только браузер)
2026-03-20 — Баг платформы: nubes_postgres_user/database зависают при destroy
Симптом
terraform destroy завершается, но в UI облака операция delete_user возвращает:
"Не удалось удалить пользователя из CRD. Производится откат"
После этого повторный terraform apply падает с:
"Нарушена консистентность: Операция вернула duplicate/exist, но объект не найден в state_out"
Причина
Платформенный баг: delete_user не удаляет CRD в кластере Kubernetes PG-оператора.
База db0 остаётся "мёртвой" — API не видит, но в кластере CRD есть.
adopt_existing_on_create = true не помогает — провайдер получает duplicate но не может прочитать объект обратно.
Решение (workaround)
Удалить PG-инстанс вручную через UI облака → почистить state → пересоздать через terraform apply.
cd ~/terra/sless/examples/POSTGRES
terraform state rm nubes_postgres_database.db
terraform state rm nubes_postgres_user.pg_user
terraform state rm nubes_postgres.npg
terraform apply -auto-approve
Статус
Зафиксировано как баг платформы. Передано девопсам для исправления в CRD-операторе PG.
2026-03-20 — Invalid index: vault_secrets["users"] на первом apply
Симптом
Error: Invalid index
on postgres.tf line 7, in locals:
7: pg_creds_map = jsondecode(nubes_postgres.npg.vault_secrets["users"])
The given key does not identify an element in this collection value.
Причина
vault_secrets["users"] появляется только после создания первого пользователя.
На первом plan/apply ключ ещё не существует.
Фикс
pg_creds_map = try(jsondecode(lookup(nubes_postgres.npg.vault_secrets, "users", "{}")), {})
pg_password = try(local.pg_creds_map[local.pg_username]["password"], "")
Пароль подтягивается при следующем apply после создания пользователя.
2026-03-17 — Баг 3: PodLogOptions compile error (v0.1.31)
Проблема: Оператор не компилировался. Ошибка:
controllers/functionjob_controller.go: unknown field Stdout in corev1.PodLogOptions
controllers/functionjob_controller.go: unknown field Stderr in corev1.PodLogOptions
Причина: corev1.PodLogOptions{} в k8s API не имеет полей Stdout и Stderr — это поля из corev1.ContainerState. Код выглядел так:
req := kube.CoreV1().Pods(namespace).GetLogs(pods.Items[0].Name, &corev1.PodLogOptions{
Stdout: true, // не существует
Stderr: true, // не существует
})
Решение: Убрать несуществующие поля. GetLogs по умолчанию возвращает stdout+stderr без дополнительных флагов:
req := kube.CoreV1().Pods(namespace).GetLogs(pods.Items[0].Name, &corev1.PodLogOptions{})
Файл: controllers/functionjob_controller.go, функция getJobPodOutput
Версия: исправлено в naeel/sless-operator:v0.1.31
2026-03-17 — Главная причина провала POSTGRES example: неправильный nubes_endpoint
Симптом
terraform apply для examples/POSTGRES падал с "job failed, check pod logs". При этом examples/simple-python и examples/hello-node работали нормально на тест-стенде.
Корневая причина
В examples/POSTGRES/main.tf в блоке provider "sless" был указан продовый endpoint Nubes API:
provider "sless" {
endpoint = "https://sless-api.kube5s.ru"
token = var.api_token
nubes_endpoint = "https://deck-api.ngcloud.ru/api/v1" # ← ПРОД
}
При этом provider "nubes" уже указывал на тест:
provider "nubes" {
api_token = var.api_token
api_endpoint = "https://deck-api-test.ngcloud.ru/api/v1/index.cfm" # ← тест ✓
}
Из-за этого sless_function и sless_job стучались в продовый Nubes API, где у тестового пользователя другой namespace и другой кластер. Postgres и pod запускались в контексте прода, а не тест-стенда. Поды не могли подключиться к базе.
Фикс
# examples/POSTGRES/main.tf
provider "sless" {
endpoint = "https://sless-api.kube5s.ru"
token = var.api_token
nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1" # ← тест
}
Аналогично исправлено в examples/simple-python/main.tf.
Почему долго искали
Долгое расследование ушло на "split-brain" — API-созданные FunctionJob'ы были невидимы через kubectl get functionjobs -A. Это объяснялось тем что:
- Job завершался (Failed) за ~5 секунд
- Между завершением и опросом kubectl объект уже мог быть очищен
- Параллельно в кластере фигурировал другой namespace (
sless-ffd1f598c169b0ae— прод) vs тест-namespace
На самом деле контроллер работал корректно. Проблема была исключительно в nubes_endpoint.
Урок
Всегда проверять: когда в main.tf два провайдера (nubes + sless) — у обоих endpoint'ы должны указывать на одну среду. Смешивание прод/тест endpoint'ов в одном apply даёт непредсказуемые результаты.
2026-03-17 — Ошибка агента: локальный запуск terraform
Проблема: Агент запускал terraform apply локально (/home/naeel/remote_dev/sless/examples/...) вместо запуска на удалённом сервере naeel@5.172.178.213.
Почему неправильно:
- Провайдер
terra.k8c.ru/naeel/slessустановлен только на удалённом сервере - Локальный terraform не имеет доступа к k8s кластеру напрямую
- Потенциально затрагивал продовые API из локальной сети
Правило: Все terraform init/plan/apply/destroy для examples/ — только через SSH на naeel@5.172.178.213:
ssh -i /home/naeel/.ssh/naeel_vm_id_ed25519 naeel@5.172.178.213 \
'cd /home/naeel/terra/sless/examples/<example> && terraform apply -auto-approve -no-color'
2026-03-17 — FunctionJob всегда "job failed, check pod logs" — расследование и два бага
Хронология расследования
Симптом: terraform apply на examples/POSTGRES завершается ошибкой:
job failed: job failed, check pod logs: kubectl logs -n sless-fn-sless-ffd1f598c169b0ae -l functionjob=pg-create-table-job-main-v12
Команда из сообщения — ничего не выдаёт (под не найден).
Ошибка агента №1 — SQL-диагностика вместо анализа кода
Первая реакция — запустить диагностические поды в кластере с psycopg2 чтобы проверить подключение к PostgreSQL. Это было неправильно: проблема не в подключении, а в том что job не запускался вообще. Пользователь остановил — "ПРИ ЧЁМ тут SQL запросы???".
Ошибка агента №2 — не читал собственный код
Раньше читались логи оператора и kubectl describe — но не исходный код контроллеров. Пользователь указал: "мы пишем ВСЁ сами, смотри в код". После прочтения functionjob_controller.go и functions.go картина сложилась за одно чтение.
Баг 1 — k8s label job-name удалён в 1.27+
Файл: controllers/functionjob_controller.go, функция getJobPodOutput
Код до:
pods, err := kube.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{
LabelSelector: "job-name=" + jobName,
})
if err != nil || len(pods.Items) == 0 {
return "completed successfully" // ← сюда всегда попадали
}
Причина: В k8s 1.27 лейбл job-name на pod-ах был deprecated, в 1.32+ удалён полностью. У нас кластер 1.34.1. Поэтому List возвращал 0 подов всегда → fallback "completed successfully" → при phase=Failed финальное сообщение "job failed, check pod logs: ...".
Логи реального сбоя никогда не попадали в status.Message FunctionJob, поэтому корневая ошибка Python-функции была невидима.
Как нашли: Проверили kubectl get pod -l "batch.kubernetes.io/job-name" — вернул поды. kubectl get pod -l "job-name" — не вернул. Версия кластера: v1.34.1 (kubectl v1.35.2).
Фикс: Использовать собственный лейбл functionjob=<fj.Name>, который мы сами выставляем на PodTemplate и который работает независимо от версии k8s.
Также: В строке 211 подсказка для пользователя тоже использовала устаревший лейбл:
"job failed, check pod logs: kubectl logs -n " + job.Namespace + " -l job-name=" + job.Name
Исправлено на functionjob=<fj.Name> — теперь команда реально работает.
Баг 2 — Split-brain cached client при CreateFunction
Файл: internal/api/handler/functions.go, функция CreateFunction
Проявление: API возвращает 409 "function already exists", а kubectl get function -A функцию не видит. terraform state rm sless_function.* не помогает — следующий terraform apply снова получает 409.
Причина: h.K8s в handler — это cached client controller-runtime. Цепочка:
- Предыдущий
terraform destroyвызвалDELETE /functions/pg-create-table-runner - Handler вызвал
h.K8s.Delete(ctx, fn)— объект удалён из etcd - Кеш controller-runtime обновляется асинхронно (informer watch). Несколько секунд объект ещё жив в памяти оператора
- Следующий
terraform apply→POST /functions→h.K8s.Create(ctx, fn)→IsAlreadyExists(кеш ещё видит объект) - Код проверяет
phase == Failed— но объект в кеше в фазеReady→ уходит в 409 навсегда
kubectl get function шёл мимо кеша (прямо в k8s API) → NotFound. API handler шёл через кеш → AlreadyExists. Поэтому они показывали разные результаты.
Фикс: При IsAlreadyExists делать uncached Get (через client.ObjectKey напрямую). Если объект реально не найден в etcd (IsNotFound) — значит кеш устарел, смело создаём заново. Если найден — возвращаем 409 как обычно (объект реально существует).
Итог
| # | Баг | Файл | Строка | Тип |
|---|---|---|---|---|
| 1 | LabelSelector: "job-name=" deprecated k8s 1.27+ |
controllers/functionjob_controller.go |
311 | Совместимость |
| 1b | Подсказка kubectl logs -l job-name= тоже устарела |
controllers/functionjob_controller.go |
211 | UX |
| 2 | Cached client → perpetual 409 при CreateFunction | internal/api/handler/functions.go |
117-133 | Race condition |
Шаблон записи
## YYYY-MM-DD — Короткое описание проблемы
**Проблема:** ...
**Причина:** ...
**Решение:** ...
2026-03-07 — kaniko: unsupported protocol scheme ""
Проблема: kaniko Job падает с unsupported protocol scheme "" при доступе к S3.
Причина: В env S3_ENDPOINT передавался хост без схемы (s3.msk-1.ngcloud.ru), kaniko ожидает полный URL.
Решение: В builder.go добавить "https://" + b.s3Endpoint при формировании env для kaniko. Также добавить S3_FORCE_PATH_STYLE=true — Ceph требует path-style URLs.
2026-03-07 — Бесконечный цикл сборки (94 job'а)
Проблема: После upload оператор создавал сотни kaniko Job'ов.
Причина: Upload handler вызывал Status().Update(phase=Pending) ПОСЛЕ того как контроллер уже выставил Building. Контроллер видел Pending → создавал новый Job, upload снова сбрасывал → цикл.
Решение:
startBuild()СНАЧАЛА ставит аннотациюlast-built-s3key = spec.S3Key(idempotency guard), затем обновляет статус.- Reconcile запускает сборку только если
spec.S3Key != annotations["last-built-s3key"]. - Upload handler убран
Status().Update()— контроллер сам управляет фазой. IsAlreadyExistsпри создании Job не считается ошибкой (parallel reconcile).
2026-03-07 — upload.go: "object has been modified; please apply your changes"
Проблема: terraform apply падал при попытке upload кода функции:
status 500: {"error":"update function: Operation cannot be fulfilled on functions.sless.kube5s.ru "pg-query": the object has been modified; please apply your changes to the latest version and try again"}
Причина: В upload.go использовался h.K8s.Update(ctx, fn). Между Get() и Update() контроллер успевал изменить объект (выставить статус/аннотации) — resourceVersion в памяти устаревал, API-сервер отклонял обновление.
Решение: Заменить Update на Patch (strategic merge patch):
patch := client.MergeFrom(fn.DeepCopy())
fn.Spec.S3Key = s3Key
fn.Spec.S3Bucket = h.S3.Bucket()
h.K8s.Patch(ctx, fn, patch)
MergePatch передаёт только изменённые поля и не требует точного resourceVersion.
2026-03-07 — Terraform Provider: "provider produced inconsistent result" для schedule
Проблема: При создании sless_trigger с type=http (без поля schedule) terraform падал:
.schedule: was null, but now cty.StringVal("")
Причина: В trToModel() провайдера Schedule всегда возвращался как types.StringValue(tr.Schedule). Если schedule не задан, API возвращает пустую строку "", провайдер сохранял "" в state. Terraform видел расхождение: в плане было null (поле не задано в HCL), в state после apply стало "".
Решение: В trToModel() возвращать types.StringNull() когда schedule пустая строка:
schedule := types.StringNull()
if tr.Schedule != "" {
schedule = types.StringValue(tr.Schedule)
}
Правило: для Optional computed-полей пустая строка из API → null в state.
2026-03-09 — source_dir: hashicorp/archive недоступен без VPN
Проблема: С VPN недоступен kube5s.ru (registry провайдера), без VPN — registry.terraform.io (hashicorp/archive). Примеры не работали ни с VPN ни без.
Причина: Зависимость от внешнего провайдера hashicorp/archive для создания zip.
Решение: Добавить атрибут source_dir в sless_function. Провайдер сам упаковывает директорию в zip (archive/zip in-memory), считает SHA256 (crypto/sha256). Файлы сортируются для детерминированного хэша. hashicorp/archive убран из всех примеров.
Provider: v0.1.10.
2026-03-09 — Destroy route cleanup bug: endpoint 502 после terraform destroy
Проблема: После terraform destroy публичный HTTP-эндпоинт продолжал отвечать — сначала 200, затем 502 function unreachable. Тестовый скрипт ждал 120 секунд и падал.
Причина: Три независимые проблемы в связке:
trigger_controller.gohandleTriggerDeletionнамеренно не удалял Service и Ingress (комментарий «оставляем — могут быть нужны другим триггерам»). Неверно: имена Service/Ingress совпадают с именем функции, а не триггера.function_controller.gohandleDeletionудалял только Deployment, не трогал Service и Ingress.invoke.goпри DNS NXDOMAIN (no such host) возвращал 502, а тестовый скрипт ждёт 404. Итог: Ingress висел вечно → 502 висел 120с.
Решение:
handleTriggerDeletion: для HTTP-триггеров явно удалятьServiceиIngressс именемfn.Spec.FunctionRefвsless-fn-{ns}.handleDeletion: добавить удалениеServiceиIngressс именемfn.Nameвsless-fn-{ns}(импортnetv1).trigger_resource.goDelete: pollingGetTriggerкаждые 3с до 90с — провайдер не возвращает успех раньше завершения cleanup.invoke.go:no such host→ HTTP 404 вместо 502 — Service удалён, эндпоинт мёртв.
Operator: v0.1.17 (контроллеры), v0.1.18 (invoke.go). Provider: v0.1.11.
2026-03-07 — Terraform Provider: "Resource Import Not Implemented"
Проблема: После failed terraform apply (Function создалась, Trigger упал) state не содержал созданных ресурсов. Повторный apply падал с status 409: function already exists. terraform import тоже не работал:
This resource does not support import. Please contact the provider developer
Причина: В провайдере не реализован ImportState для ресурса sless_function.
Решение (краткосрочное): Удалить ресурсы из кластера вручную (kubectl delete) и повторить apply с чистым state.
Решение (долгосрочное / TODO): Реализовать ImportState для sless_function и sless_trigger:
func (r *FunctionResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
// ID формат: "namespace/name"
resource.ImportStatePassthroughID(ctx, path.Root("name"), req, resp)
}
2026-03-07 — handler.py: column "started_at" does not exist
Проблема: При вызове функции pg-query в логах пода:
psycopg2.errors.UndefinedColumn: column "started_at" does not exist
Причина: В examples/pg-query/handler.py использовалось имя колонки started_at, но в схеме migrations/001_initial.sql таблицы invocations колонка называется created_at.
Решение: Исправить select и dict() в handler.py: started_at → created_at.
Урок: При написании примеров всегда сверяться со схемой в migrations/.
2026-03-07 — Deployment не перезапустил pod после re-build образа
Проблема: После terraform apply (re-upload + kaniko пересборка) pod продолжал использовать старый код — curl возвращал ошибку started_at.
Причина: Образ тегируется как :latest. Kubernetes не перезапускает pod автоматически если тег не изменился — даже если образ на DockerHub обновился. imagePullPolicy по умолчанию IfNotPresent для non-digest образов, только Always гарантирует pull при каждом запуске.
Решение (краткосрочное): Явный kubectl rollout restart deployment/pg-query -n sless-fn-default.
Решение (долгосрочное / TODO): В function_controller.go после успешной сборки добавить rollout restart аннотацию:
// Форсируем rollout через аннотацию kubectl.kubernetes.io/restartedAt
deployment.Spec.Template.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339)
Либо использовать версионированные теги образов вместо :latest (:v{timestamp}).
2026-03-08 — output_md5 archive провайдера не обновляется при изменении кода
Проблема: terraform plan показывал "No changes" даже после изменения JS-файла функции. Функция отдавала старый код.
Причина: code_hash = data.archive_file.handler_http.output_md5 — атрибут output_md5 в hashicorp/archive v2.7.x возвращает MD5 от предыдущей версии zip (баг порядка вычисления). output_sha и output_sha256 от того же файла обновлялись корректно.
Решение: Заменить output_md5 на filesha256("${path.module}/code/handler.js") — хэшируется исходный файл напрямую, минуя archive провайдер.
Урок: Не использовать output_md5 из archive_file. Использовать filesha256(source_file).
2026-03-08 — FunctionJob зависал в Running навсегда
Проблема: terraform apply висел на sless_job.hello_run: Still creating... бесконечно. k8s Job давно завершился (Complete), но FunctionJob оставался в Running.
Причина: SetupWithManager использовал Owns(&batchv1.Job{}) для watch событий Job. Но Job создавался в namespace sless-fn-default, а FunctionJob в default — cross-namespace OwnerReference не работают в k8s. Reconcile после завершения Job никогда не вызывался.
Решение: Убрать Owns(&batchv1.Job{}). В syncJobStatus добавить return ctrl.Result{RequeueAfter: 5 * time.Second} когда Job ещё выполняется — polling каждые 5 сек.
2026-03-08 — ImagePullBackOff: sless-registry-auth не копировался в sless-fn-*
Проблема: Pod функции падал с ImagePullBackOff — не мог скачать образ из registry.
Причина: buildDeployment в function_controller.go не устанавливал imagePullSecrets в PodSpec. Секрет sless-registry-auth существовал только в namespace sless, но не копировался в sless-fn-default.
Решение: Добавить ensureRegistrySecret() — копирует секрет из sless в sless-fn-* при каждом reconcile. В buildDeployment добавить ImagePullSecrets.
2026-03-08 — CrashLoopBackOff: Cannot find module handler.js
Проблема: Pod функции падал: Cannot find module '/app/function/handler.js'. Entrypoint был handler-http.handle (файл handler-http.js), а server.js искал хардкоженный handler.js.
Причина: server.js и server.py runtime-образов хардкодили имя модуля handler. Поле SLESS_ENTRYPOINT env var не использовалось.
Решение: server.js и server.py переписаны: парсят SLESS_ENTRYPOINT (формат "module.funcName"), динамически загружают нужный модуль. Runtime пересобран как v0.1.1.
2026-03-08 — Kaniko cache: пересобирал образ со старым server.js
Проблема: После исправления server.js kaniko продолжал собирать образы с старым server.js из кэша.
Причина: Base image тегирован как :latest. Kaniko кэшировал слои — FROM naeel/sless-runtime-nodejs20:latest не перечитывал обновлённый образ.
Решение: Новый тег базового образа v0.1.1. В upload.go изменить runtimeBaseImage("nodejs20") → naeel/sless-runtime-nodejs20:v0.1.1.
2026-03-08 — RBAC: secrets is forbidden: cannot list
Проблема: Оператор падал: secrets is forbidden: User "system:serviceaccount:sless:sless-operator" cannot list resource "secrets".
Причина: В rbac.yaml для secrets были только get;create, нужны также list;watch для controller-runtime.
Решение: Добавить list;watch в ClusterRole для resources: ["secrets"].
2026-03-08 — Harbor registry нестабилен (504 Gateway Timeout)
Проблема: Kaniko падал: GET https://pearlharbor.registryk8s.services.ngcloud.ru/v2/: unexpected status code 504 Gateway Timeout. Периодически /v2/ зависал на 10+ секунд или не отвечал вовсе.
Причина: Harbor — внешний сервис облачного провайдера, нестабилен по независящим от нас причинам. Поведение не зависит от VPN.
Решение: Переключить REGISTRY_HOST с Harbor на DockerHub (naeel). Образы функций теперь пушатся как naeel/sless-default-{namespace}-{name}:latest.
2026-03-08 — Provider: inconsistent result — image_ref изменился после apply
Проблема: После terraform apply ошибка:
Provider produced inconsistent result after apply
.image_ref: was cty.StringVal("pearlharbor..."), but now cty.StringVal("naeel/...")
Причина: image_ref в схеме провайдера имел UseStateForUnknown(). При plan terraform брал значение из state (старый Harbor путь). После apply API возвращал новый DockerHub путь. Расхождение план vs результат.
Решение: Убрать UseStateForUnknown() с image_ref. Теперь image_ref = (known after apply) при каждом apply — честно отражает что значение неизвестно до сборки. Провайдер v0.1.5.
2026-03-08 — Provider: inconsistent result — phase Failed→Ready
Проблема:
Provider produced inconsistent result after apply
.phase: was cty.StringVal("Failed"), but now cty.StringVal("Ready")
Причина: При ошибке WaitReady (build failed) → AddError → return. resp.State.Set не вызывался → state не обновлялся → в state оставалось старое значение. Затем оператор замечал новый код, пересобирал → функция становилась Ready. Следующий Read возвращал phase=Ready, terraform видел расхождение.
Решение: Убрать UseStateForUnknown() с image_ref (связанный фикс). Для phase — UseStateForUnknown() оставлен, т.к. при нормальном flow Ready→Ready предсказуем.
2026-03-08 — Стейл-сервис hello-node блокировал proxy запросы
Проблема: GET /fn/default/hello-node возвращал operation not permitted — dial tcp 10.100.77.255:8080: connect: operation not permitted.
Причина: Сервис hello-node в sless-fn-default существовал 26h с ClusterIP 10.100.77.255 но без Deployment/Pod. iptables дропал пакеты на несуществующий endpoint.
Решение: kubectl delete svc hello-node -n sless-fn-default.
2026-03-07 — Docker credStore: exec "docker-credential-desktop.exe"
Проблема: docker push падал с exec: "docker-credential-desktop.exe": executable file not found.
Причина: В ~/.docker/config.json осталась запись "credsStore": "desktop.exe" от Docker Desktop.
Решение: Удалить поле credsStore из ~/.docker/config.json, оставить только auths с base64 credentials.
2026-03-07 — controller-gen v0.11.1 паникует с Go 1.23
Проблема: operator-sdk create api падает с panic в controller-gen при make generate.
Причина: controller-tools v0.11.1 несовместим с Go 1.23 (nil pointer dereference в go/types).
Решение: В Makefile заменить CONTROLLER_TOOLS_VERSION ?= v0.11.1 на v0.14.0.
2026-03-07 — Dockerfile: operator не находит migrations/ при старте
Проблема: Оператор в кластере сразу падает в CrashLoopBackOff:
level=ERROR msg="read migration file" err="open migrations/001_initial.sql: no such file or directory"
Причина: Оригинальный Dockerfile копировал только api/, controllers/, main.go. Директории internal/ и migrations/ в образ не попадали.
Решение: Добавить в оба stage Dockerfile:
COPY internal/ internal/ # в builder stage
COPY migrations/ migrations/ # и в builder, и в финальный alpine stage
migrations/ нужен в финальном образе — оператор читает SQL-файлы в runtime при каждом старте.
2026-03-07 — Dockerfile: golang:1.23 несовместим с go.mod (требует 1.25)
Проблема: docker build падает на go mod download:
go: go.mod requires go >= 1.25 (running go 1.23.12; GOTOOLCHAIN=local)
Причина: В go.mod указан go 1.25, Dockerfile использовал golang:1.23-alpine.
Решение: FROM golang:1.23-alpine → FROM golang:1.25-alpine.
2026-03-07 — go.mod: прямые зависимости помечены как indirect
Проблема: gopls показывал ошибки в go.mod:
github.com/gorilla/mux should be direct
github.com/lib/pq should be direct
github.com/minio/minio-go/v7 should be direct
k8s.io/api should be direct
Причина: Пакеты получили // indirect несмотря на прямые импорты — побочный эффект scaffold-генерации operator-sdk.
Решение: go mod tidy автоматически расставил правильные аннотации.
2026-03-08 — После kaniko rebuild функция возвращает старый код
Проблема: terraform apply с изменённым кодом успешно завершал kaniko build, но функция продолжала возвращать старую версию кода.
Причина: imagePullPolicy: IfNotPresent (default в k8s) + :latest тег — после успешной сборки оператор обновлял .spec.template.spec.containers[0].image, но image tag не менялся (всегда :latest). Kubernetes видел что образ уже есть на ноде и не пул-ил новый. Pod оставался запущенным со старым образом.
Решение: В controllers/function_controller.go, функция ensureDeployment, после обновления image добавлена аннотация:
existing.Spec.Template.Annotations["kubectl.kubernetes.io/restartedAt"] = fn.Status.LastBuiltAt.Time.Format(time.RFC3339)
Аннотация меняется при каждой новой сборке (значение = lastBuiltAt) → Kubernetes делает rolling restart → kubelet пул-ит свежий образ.
Оператор: v0.1.11.
2026-03-09 — Негативные тесты API: найденные баги валидации
Методология тестирования
Тесты запускались через прямые POST-запросы к API (https://sless-api.kube5s.ru)
со специально некорректными параметрами. Скрипт: /tmp/sless_negative_tests.sh.
Результаты
| # | Тест | Ожидание | Факт | Статус |
|---|---|---|---|---|
| T1 | runtime="python999" |
400 ошибка | Unsupported value: "python999" |
✅ |
| T2 | memory_mb=-10 |
400 ошибка | 201 Created — принято без ошибки | ❌ БАГ |
| T3 | name="" |
400 ошибка | name and runtime are required |
✅ |
| T4 | entrypoint="" (не передан) |
400 ошибка | 201 Created с entrypoint:"" |
❌ БАГ |
| T5 | trigger → несуществующая fn | 404/400 | name, type and function are required |
❌ Неверный текст ошибки |
| T6 | job → несуществующая fn | 404/400 | Создался (fn не проверяется при создании job) | ⚠️ |
| T7 | trigger type="rabbitmq" |
400 ошибка | name, type and function are required |
❌ Неверный текст ошибки |
| T8 | run_id=0 |
job создаётся, не запускается | 201, run_id:0 |
✅ |
| T9 | несуществующий namespace | 404 | namespaces "nonexistent-ns" not found |
✅ |
| T10 | memory_mb=999999 |
400 или ограничение | 201, вернул memory_mb=128 (молча обрезан) | ❌ БАГ |
Баги для исправления
БАГ-1 (API handler/function): memory_mb <= 0 не валидируется. Нужно: if req.MemoryMB <= 0 { return 400 }.
БАГ-2 (API handler/function): entrypoint == "" не валидируется. Нужно: проверять непустой entrypoint.
БАГ-3 (API handler/trigger): При передаче type="rabbitmq" или несуществующей functionRef ошибка говорит "name, type and function are required" — неверный текст. Скорее всего handler проверяет поле function_ref (snake_case) а принимает functionRef (camelCase) — маппинг не работает.
БАГ-4 (API handler/function): memory_mb=999999 принято, но возвращён memory_mb=128 — молчаливое изменение без ошибки.
Дополнительно: валидация в Terraform провайдере (plan-time)
Провайдер не имел schema-level валидаторов — все ошибки проявлялись только на apply.
Добавлены validators в function_resource.go, trigger_resource.go, job_resource.go:
runtime— только допустимые значенияmemory_mb— диапазон 64-4096timeout_sec— диапазон 1-900trigger.type— толькоhttp/cronrun_id— >= 0
Исправления (2026-03-08): v0.1.13 оператор + v0.1.7 провайдер
API исправления (internal/api/handler/)
БАГ-1 FIXED (functions.go): Добавлена валидация memory_mb:
if req.MemoryMB <= 0 || req.MemoryMB > 4096 → 400 "memory_mb must be between 1 and 4096"
БАГ-2 FIXED (functions.go): Добавлена валидация entrypoint:
if req.Entrypoint == "" → 400 "entrypoint is required"
БАГ-3 FIXED (triggers.go): Добавлена валидация type:
if req.Type != "http" && req.Type != "cron" → 400 "type must be \"http\" or \"cron\""
БАГ-4 FIXED (functions.go): Покрыт случаем T10 — memory_mb=999999 теперь возвращает 400.
Провайдер план-валидаторы (terraform/provider/)
Добавлен пакет terraform-plugin-framework-validators v0.19.0.
function_resource.go:
runtime—stringvalidator.OneOf("nodejs20", "python3.11", "go1.21")memory_mb—int64validator.Between(1, 4096)timeout_sec—int64validator.Between(1, 900)
trigger_resource.go:
type—stringvalidator.OneOf("http", "cron")
job_resource.go:
run_id—int64validator.AtLeast(0)
Подтверждение повторными тестами (API)
| Тест | Ожидание | Результат |
|---|---|---|
T2: memory_mb=-10 |
400 | ✅ {"error":"memory_mb must be between 1 and 4096"} |
T4: entrypoint="" |
400 | ✅ {"error":"entrypoint is required"} |
T7: type="rabbitmq" |
400 + правильное сообщение | ✅ {"error":"type must be \"http\" or \"cron\""} |
T10: memory_mb=999999 |
400 | ✅ {"error":"memory_mb must be between 1 and 4096"} |
Подтверждение план-валидаторов (Terraform)
Тест: runtime = "python2.7" в http.tf →
Error: Invalid Attribute Value Match
Attribute runtime value must be one of: ["nodejs20" "python3.11" "go1.21"], got: "python2.7"
Версии
- Оператор:
naeel/sless-operator:v0.1.13 - Провайдер:
terra.k8c.ru/naeel/sless v0.1.7
2026-03-19 — Баг 4: Хардкодный 30s таймаут в invoke.go → context deadline exceeded
Симптом
Вызов функции stress-go-pgstorm с duration_sec=30 возвращал:
{"error": "function unreachable: ... context deadline exceeded"}
При этом под был Running, логи показывали нормальную работу pgxpool.
Корневая причина
В internal/api/handler/invoke.go (строка 24) был глобальный http.Client:
var httpClient = &http.Client{Timeout: 30 * time.Second}
Функция реально отрабатывала ровно 30 секунд (duration_sec=30) + накладные расходы на pgxpool.New() и первый коннект к БД ≈ 1-2 секунды. Итого запрос превышал 30s → оператор разрывал соединение раньше чем функция успевала ответить.
Почему так было написано
При создании invoke.go в марте 2026 таймаут 30s считался "достаточным для холодного старта". Длительные функции тогда не планировались. Когда появились batch/stress задачи с timeout_sec=600-700 — баг стал критическим.
Решение
Убрать глобальный httpClient. Перед каждым вызовом:
- Получить Function CRD из k8s:
h.K8s.Get(ctx, ObjectKey{name, ns}, fn) - Прочитать
fn.Spec.TimeoutSec - Создать
http.Client{Timeout: TimeoutSec*time.Second + 5*time.Second} - Если функция не найдена (Get вернул ошибку) — дефолт 30s
func invokeHTTPClient(timeoutSec int32) *http.Client {
t := time.Duration(timeoutSec)*time.Second + 5*time.Second
if timeoutSec <= 0 {
t = 30 * time.Second
}
return &http.Client{Timeout: t}
}
Файл
internal/api/handler/invoke.go — исправлено в коммите d7fda15
Оператор пересобран: naeel/sless-operator:v0.1.40
Урок
Никогда не хардкодить таймауты в прокси-слое. Таймаут всегда должен браться
из конфигурации вызываемого ресурса. Function.Spec.TimeoutSec существует именно для этого.
2026-03-19 — Баг 5: nginx ingress proxy-read-timeout не задан → 504 Gateway Time-out
Симптом
После фикса invoke.go (баг 4) — повторный вызов stress-go-pgstorm вернул:
<html><head><title>504 Gateway Time-out</title></head>
<body><center><h1>504 Gateway Time-out</h1></center>
<hr><center>nginx</center></body></html>
curl exit code 5 (не 0), процесс завершился через ~60 секунд после старта запроса.
Корневая причина
У ingress sless-operator не было аннотации proxy-read-timeout.
nginx ingress controller использует дефолт 60 секунд если аннотация отсутствует.
# Было — аннотаций timeout нет вообще:
annotations:
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
Цепочка: клиент → nginx (60s timeout) → оператор (705s) → функция (600s). Nginx оборвал соединение на 60-й секунде, хотя и оператор и функция были живы.
Диагностика
Проверили аннотации всех ingress в ns sless:
noderedingress:proxy-read-timeout: "3600"✅ (кто-то правильно настроил)sless-funcs-ingress: нет timeout аннотацийsless-operator: нет timeout аннотаций ← виновник
Решение
kubectl annotateдля мгновенного применения:
kubectl annotate ingress sless-operator -n sless \
nginx.ingress.kubernetes.io/proxy-read-timeout="900" \
nginx.ingress.kubernetes.io/proxy-send-timeout="900" --overwrite
- Сохранить в манифест
deployments/k8s/operator.yaml:
nginx.ingress.kubernetes.io/proxy-read-timeout: "900"
nginx.ingress.kubernetes.io/proxy-send-timeout: "900"
Почему 900s
- function timeout_sec = 700 → оператор ждёт 705s
- nginx должен ждать дольше чем оператор → 900s с запасом
- Не ставим 3600s как у nodered — избыточно для функций
Файл
deployments/k8s/operator.yaml — обновлено в коммите d7fda15
Урок
При развёртывании нового ingress всегда явно задавать proxy-read-timeout
и proxy-send-timeout. Nginx дефолт 60s подходит только для быстрых API.
Для любых операций дольше 30s — обязательны явные таймауты.
2026-03-22 — G13/G14/G15: Баги найденные тестами (v0.1.50 → v0.1.51)
БАГ-1: CreateService не возвращал 400 при невалидном runtime (ruby3.0)
Обнаружен: G12 failure test (43/45), тест G12-F-9
Симптом: POST /services с runtime: ruby3.0 → 500 вместо 400
Причина: h.K8s.Create() вызывает webhook-валидацию CRD; kubernetes возвращает errors.IsInvalid при отклонённом значении enum, но в CreateService не было обработки этого типа ошибки — она падала в generic 500.
Исправление: internal/api/handler/services.go, добавлен блок:
if errors.IsInvalid(err) {
writeJSON(w, http.StatusBadRequest, errResp("invalid service spec: "+err.Error()))
return
}
Версия: v0.1.50
БАГ-2: SLESS_ENTRYPOINT не передавался в Deployment
Обнаружен: G12 failure test (43/45), тест G12-F-2
Симптом: Функция запускалась, но entrypoint игнорировался — runtime не знал какой handler вызывать
Причина: buildServiceDeployment строил envVars только из svc.Spec.Env, переменная SLESS_ENTRYPOINT не добавлялась
Исправление: controllers/service_controller.go, в buildServiceDeployment:
envVars = append(envVars, corev1.EnvVar{Name: "SLESS_ENTRYPOINT", Value: svc.Spec.Entrypoint})
Версия: v0.1.50
БАГ-3: UpdateService не возвращал 400 при невалидном runtime (ruby3.0)
Обнаружен: G13 edge cases test (G13E-5), тест: PUT ruby3.0 → 500
Симптом: PUT /services/{name} с runtime: ruby3.0 → 500 вместо 400
Причина: UpdateService вызывает h.K8s.Update() который тоже возвращает IsInvalid, но обработка не была добавлена — только CreateService был исправлен в v0.1.50
Исправление: internal/api/handler/services.go, UpdateService:
if errors.IsInvalid(err) {
writeJSON(w, http.StatusBadRequest, errResp("invalid service spec: "+err.Error()))
return
}
Версия: v0.1.51
ПСЕВДО-БАГ: G13F-2 upload empty body → 404 (баг теста, не кода)
Симптом: POST тест шлёт пустой multipart без -X POST → curl делает GET → gorilla/mux возвращает 404
Причина: В скрипте не было -X POST для curl при тесте пустого тела
Исправление: Добавлен -X POST в curl-команду теста
ПСЕВДО-БАГ: G13D-3 GET /fn/ → FAIL (not a bug, by design)
Симптом: Тест ожидал 405 при GET invoke, но получал 200
Причина: router.go строка 25 явно комментирует: "Все HTTP методы разрешены (GET/POST/PUT/... — решает сама функция)"
Исправление: Тест обновлён — pass при любом коде (задокументировано как by design)
ПСЕВДО-БАГ: G15C-2 список сервисов → 0 объектов (баг теста)
Симптом: Python-код пытался обратиться к .get('items', []) но API возвращает [] напрямую (не {"items": [...]})
Причина: Неверное предположение о структуре ответа GET /services
Исправление: Тест исправлен — обрабатывает и массив и объект с полем items
ПСЕВДО-БАГ: G15E-3 DELETE → 204 (баг теста, не кода)
Симптом: Тест ожидал 200, сервер возвращал 204
Причина: DeleteService правильно возвращает HTTP 204 No Content (REST-стандарт для DELETE)
Исправление: Тест принимает 204 и 200
ПСЕВДО-БАГ: G15D 503 сразу после kill operator pod (expected behavior)
Симптом: После kubectl delete pod оператора API возвращает 503 (не 400/404/409)
Причина: Operator pod = API server. Пока старый pod завершается и новый не поднялся — ingress/proxy отдаёт 503
Исправление: Тест принимает 503/502 как валидный транзиентный ответ с NOTE