API (operator v0.1.13): - functions.go: добавлена валидация entrypoint (не пустой) и memory_mb (1-4096). Фиксирует БАГ-1/2/4 из негативных тестов. - triggers.go: добавлена валидация type (только 'http'/'cron'). Фиксирует БАГ-3 (неверное сообщение об ошибке). Провайдер (v0.1.7): - Добавлен пакет terraform-plugin-framework-validators v0.19.0 - function_resource: runtime OneOf, memory_mb 1-4096, timeout_sec 1-900 - trigger_resource: type OneOf(http, cron) - job_resource: run_id AtLeast(0) - examples/main.tf: обновлена версия до ~> 0.1.7 doc/errors/log.md: задокументированы исправления и результаты повторных тестов
24 KiB
Ошибки и решения
Сюда записываем проблемы с которыми столкнулись и как их решили.
Шаблон записи
## 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-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