From 0268ef052377ad4b7e3bec813404034ad7e2c30d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Sat, 7 Mar 2026 10:52:29 +0400 Subject: [PATCH] fix: multiple fixes + pg-query e2e passed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit upload.go: - Update → Patch (MergeFrom) to avoid resourceVersion conflict when operator modifies Function between Get() and Update() terraform provider v0.1.1: - trigger_resource.go: trToModel returns StringNull() for empty schedule (fixes 'provider produced inconsistent result' for http triggers) - main.go: bump version to 0.1.1 examples/pg-query: - handler.py: fix column name started_at → created_at (matches migrations/001) - main.tf: pin provider to ~> 0.1.1 .gitignore: add terraform state, lock, .terraform/, handler.zip doc/errors/log.md: documented all 5 errors from this session: - resourceVersion conflict → use Patch - terraform inconsistent result for schedule → StringNull - terraform import not implemented → delete+recreate workaround - wrong column name → check migrations before writing handlers - Deployment not restarting after image rebuild → rollout restart / TODO: restartedAt annotation doc/progress.md: terraform apply e2e ✅ --- .gitignore | 7 ++ doc/errors/log.md | 93 +++++++++++++++++++ doc/progress.md | 2 +- examples/pg-query/handler.py | 6 +- examples/pg-query/main.tf | 2 +- internal/api/handler/upload.go | 8 +- .../internal/resources/trigger_resource.go | 8 +- terraform/provider/main.go | 4 +- 8 files changed, 120 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 07c35fd..20a2dd2 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,10 @@ Dockerfile.cross *.swo *~ terraform/provider/build/ + +# Terraform state и кэш провайдеров (не коммитим) +**/.terraform/ +**/.terraform.lock.hcl +**/terraform.tfstate +**/terraform.tfstate.backup +**/handler.zip diff --git a/doc/errors/log.md b/doc/errors/log.md index b2ca9d8..9e7ee3d 100644 --- a/doc/errors/log.md +++ b/doc/errors/log.md @@ -38,6 +38,99 @@ --- +## 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-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-07 — Docker credStore: exec "docker-credential-desktop.exe" **Проблема:** `docker push` падал с `exec: "docker-credential-desktop.exe": executable file not found`. diff --git a/doc/progress.md b/doc/progress.md index 1e959ad..43fff48 100644 --- a/doc/progress.md +++ b/doc/progress.md @@ -79,7 +79,7 @@ | # | Компонент | Статус | Заметки | |---|-----------|--------|---------| -| 1 | terraform apply e2e — функция с postgres | 🔄 В процессе | examples/pg-query/{handler.py,requirements.txt,main.tf} созданы; upload.go поддерживает requirements.txt; поднять среду + terraform apply | +| 1 | terraform apply e2e — функция с postgres | ✅ | `{"invocations": [], "count": 0}` — функция подключилась к postgres.sless.svc, таблица пустая | | 2 | HTTP trigger e2e тест | ⏳ | Service+Ingress через TriggerController | | 3 | deployments/k8s/operator.yaml | ⏳ | Deployment оператора в кластере | | 4 | Перенос ресурсов sless в nubes провайдер | ⏳ | после e2e тестирования | diff --git a/examples/pg-query/handler.py b/examples/pg-query/handler.py index 8007e2a..b0a5835 100644 --- a/examples/pg-query/handler.py +++ b/examples/pg-query/handler.py @@ -24,15 +24,15 @@ def handle(event): conn = psycopg2.connect(dsn) try: cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) - cur.execute("SELECT id, namespace, function_name, status, started_at FROM invocations ORDER BY started_at DESC LIMIT %s", (limit,)) + cur.execute("SELECT id, namespace, function_name, status, created_at FROM invocations ORDER BY created_at DESC LIMIT %s", (limit,)) rows = cur.fetchall() # RealDictCursor возвращает объекты, сериализуем вручную result = [] for row in rows: r = dict(row) # datetime → str - if r.get("started_at"): - r["started_at"] = str(r["started_at"]) + if r.get("created_at"): + r["created_at"] = str(r["created_at"]) result.append(r) return {"invocations": result, "count": len(result)} finally: diff --git a/examples/pg-query/main.tf b/examples/pg-query/main.tf index a545b2f..7c5408c 100644 --- a/examples/pg-query/main.tf +++ b/examples/pg-query/main.tf @@ -16,7 +16,7 @@ terraform { required_providers { sless = { source = "terra.k8c.ru/naeel/sless" - version = "~> 0.1" + version = "~> 0.1.1" } } } diff --git a/internal/api/handler/upload.go b/internal/api/handler/upload.go index 95244e6..844ce92 100644 --- a/internal/api/handler/upload.go +++ b/internal/api/handler/upload.go @@ -184,10 +184,14 @@ func (h *Handler) UploadCode(w http.ResponseWriter, r *http.Request) { return } - // Обновляем Function CRD: новый s3Key и bucket → контроллер запустит kaniko + // Обновляем Function CRD: новый s3Key и bucket → контроллер запустит kaniko. + // Используем Patch вместо Update чтобы избежать конфликта resourceVersion: + // между Get() выше и Update() контроллер может изменить статус объекта. + // MergePatch обновляет только указанные поля, не требует точного resourceVersion. + patch := client.MergeFrom(fn.DeepCopy()) fn.Spec.S3Key = s3Key fn.Spec.S3Bucket = h.S3.Bucket() - if err := h.K8s.Update(r.Context(), fn); err != nil { + if err := h.K8s.Patch(r.Context(), fn, patch); err != nil { writeJSON(w, http.StatusInternalServerError, errResp("update function: "+err.Error())) return } diff --git a/terraform/provider/internal/resources/trigger_resource.go b/terraform/provider/internal/resources/trigger_resource.go index dde7fad..55c2018 100644 --- a/terraform/provider/internal/resources/trigger_resource.go +++ b/terraform/provider/internal/resources/trigger_resource.go @@ -172,13 +172,19 @@ func (r *TriggerResource) Delete(ctx context.Context, req resource.DeleteRequest } // trToModel конвертирует API-ответ → state модель. +// schedule: возвращаем Null если пустая строка — terraform требует null для Optional-полей, +// которые не заданы в конфиге (иначе план: null → apply: "" → "inconsistent result"). func trToModel(tr *client.TriggerResponse) TriggerModel { + schedule := types.StringNull() + if tr.Schedule != "" { + schedule = types.StringValue(tr.Schedule) + } return TriggerModel{ Namespace: types.StringValue(tr.Namespace), Name: types.StringValue(tr.Name), Type: types.StringValue(tr.Type), FunctionRef: types.StringValue(tr.FunctionRef), - Schedule: types.StringValue(tr.Schedule), + Schedule: schedule, Active: types.BoolValue(tr.Active), URL: types.StringValue(tr.URL), } diff --git a/terraform/provider/main.go b/terraform/provider/main.go index 20e129f..a1084eb 100644 --- a/terraform/provider/main.go +++ b/terraform/provider/main.go @@ -12,8 +12,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/providerserver" ) -// version задаётся ldflags при сборке: -ldflags "-X main.version=0.1.0" -var version string = "0.1.0" +// version задаётся ldflags при сборке: -ldflags "-X main.version=0.1.1" +var version string = "0.1.1" func main() { opts := providerserver.ServeOpts{