Files
sless/doc/errors/log.md
T
“Naeel” 80991d2aab docs: update progress.md, errors/log.md; add gitignore for dist/ and tfstate backups
- doc/progress.md: обновлена версия до operator v0.1.18 / provider v0.1.11,
  добавлен блок source_dir + fix destroy cleanup (2026-03-09),
  уточнены комментарии к контроллерам и trigger_resource
- doc/errors/log.md: добавлены две записи — source_dir/hashicorp/archive,
  destroy route cleanup bug (три причины + решения)
- .gitignore: добавлены examples/*/dist/ и terraform.tfstate.*.backup
- examples/: удалены dist/ zip и tfstate.*.backup из трекинга
2026-03-09 20:29:27 +04:00

450 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Ошибки и решения
> Сюда записываем проблемы с которыми столкнулись и как их решили.
## Шаблон записи
```
## 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):
```go
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 пустая строка:
```go
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`:
```go
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` аннотацию:
```go
// Форсируем 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:
```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 добавлена аннотация:
```go
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`:
- `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`