Files
sless/doc/errors/log.md
T
Naeel 9b74358979 docs: lifecycle test — 44/44 PASS; 2 бага оператора задокументированы в errors/log.md + progress.md
Баги:
  1. memory_mb через PUT не обновляет k8s Deployment Resources → controllers/service_controller.go:~241
  2. нет self-healing при ручном удалении Deployment → нет RequeueAfter / cross-namespace watch
2026-03-21 18:32:14 +03:00

53 KiB
Raw Blame History

Ошибки и решения

Сюда записываем проблемы с которыми столкнулись и как их решили.


2026-03-21 — БАГ: memory_mb через PUT не применяется в k8s Deployment (НЕ ИСПРАВЛЕН)

Симптом

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 он не копируется.

Фикс (не применён)

// В 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

Файл

controllers/service_controller.go ~ строка 241

Обнаружен

operator_lifecycle_test.sh, тест 2.13


2026-03-21 — БАГ: нет self-healing — ручное удаление Deployment не восстанавливается (НЕ ИСПРАВЛЕН)

Симптом

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 не генерирует.

Фикс (вариант)

Добавить RequeueAfter: 60s в ensureServiceDeployment — тогда контроллер будет периодически проверять и пересоздавать:

// В конце ensureServiceDeployment:
return ctrl.Result{RequeueAfter: 60 * time.Second}, nil

Или использовать Watches() с cross-namespace mapper (сложнее).

Файл

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.goDeleteFunction
  • internal/api/handler/services.goDeleteService
  • internal/api/handler/triggers.goDeleteTrigger
  • internal/api/handler/jobs.goDeleteJob

Фикс

Коммит 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.

Фикс

  1. Оператор (internal/api/handler/source.go): добавлен GetServiceSource — делает то же что GetSource, но читает Service CRD вместо Function.
  2. Оператор (internal/api/router.go): зарегистрирован маршрут GET /v1/namespaces/{ns}/services/{name}/source.
  3. funcs-service (services/funcs/main.go): proxySourceGet принимает параметр kind; при kind=service обращается к /services/…/source.
  4. 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-testdeck-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. Это объяснялось тем что:

  1. Job завершался (Failed) за ~5 секунд
  2. Между завершением и опросом kubectl объект уже мог быть очищен
  3. Параллельно в кластере фигурировал другой 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. Цепочка:

  1. Предыдущий terraform destroy вызвал DELETE /functions/pg-create-table-runner
  2. Handler вызвал h.K8s.Delete(ctx, fn) — объект удалён из etcd
  3. Кеш controller-runtime обновляется асинхронно (informer watch). Несколько секунд объект ещё жив в памяти оператора
  4. Следующий terraform applyPOST /functionsh.K8s.Create(ctx, fn)IsAlreadyExists (кеш ещё видит объект)
  5. Код проверяет 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 снова сбрасывал → цикл.

Решение:

  1. startBuild() СНАЧАЛА ставит аннотацию last-built-s3key = spec.S3Key (idempotency guard), затем обновляет статус.
  2. Reconcile запускает сборку только если spec.S3Key != annotations["last-built-s3key"].
  3. Upload handler убран Status().Update() — контроллер сам управляет фазой.
  4. 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 секунд и падал.

Причина: Три независимые проблемы в связке:

  1. trigger_controller.go handleTriggerDeletion намеренно не удалял Service и Ingress (комментарий «оставляем — могут быть нужны другим триггерам»). Неверно: имена Service/Ingress совпадают с именем функции, а не триггера.
  2. function_controller.go handleDeletion удалял только Deployment, не трогал Service и Ingress.
  3. invoke.go при DNS NXDOMAIN (no such host) возвращал 502, а тестовый скрипт ждёт 404. Итог: Ingress висел вечно → 502 висел 120с.

Решение:

  1. handleTriggerDeletion: для HTTP-триггеров явно удалять Service и Ingress с именем fn.Spec.FunctionRef в sless-fn-{ns}.
  2. handleDeletion: добавить удаление Service и Ingress с именем fn.Name в sless-fn-{ns} (импорт netv1).
  3. trigger_resource.go Delete: polling GetTrigger каждые 3с до 90с — провайдер не возвращает успех раньше завершения cleanup.
  4. 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_atcreated_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) → AddErrorreturn. resp.State.Set не вызывался → state не обновлялся → в state оставалось старое значение. Затем оператор замечал новый код, пересобирал → функция становилась Ready. Следующий Read возвращал phase=Ready, terraform видел расхождение.

Решение: Убрать UseStateForUnknown() с image_ref (связанный фикс). Для phaseUseStateForUnknown() оставлен, т.к. при нормальном flow Ready→Ready предсказуем.


2026-03-08 — Стейл-сервис hello-node блокировал proxy запросы

Проблема: GET /fn/default/hello-node возвращал operation not permitteddial 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-alpineFROM 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-4096
  • timeout_sec — диапазон 1-900
  • trigger.type — только http/cron
  • run_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:

  • runtimestringvalidator.OneOf("nodejs20", "python3.11", "go1.21")
  • memory_mbint64validator.Between(1, 4096)
  • timeout_secint64validator.Between(1, 900)

trigger_resource.go:

  • typestringvalidator.OneOf("http", "cron")

job_resource.go:

  • run_idint64validator.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. Перед каждым вызовом:

  1. Получить Function CRD из k8s: h.K8s.Get(ctx, ObjectKey{name, ns}, fn)
  2. Прочитать fn.Spec.TimeoutSec
  3. Создать http.Client{Timeout: TimeoutSec*time.Second + 5*time.Second}
  4. Если функция не найдена (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:

  • nodered ingress: proxy-read-timeout: "3600" (кто-то правильно настроил)
  • sless-funcs-ingress: нет timeout аннотаций
  • sless-operator: нет timeout аннотаций ← виновник

Решение

  1. 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
  1. Сохранить в манифест 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 — обязательны явные таймауты.