fix: multiple fixes + pg-query e2e passed
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 ✅
This commit is contained in:
@@ -29,3 +29,10 @@ Dockerfile.cross
|
||||
*.swo
|
||||
*~
|
||||
terraform/provider/build/
|
||||
|
||||
# Terraform state и кэш провайдеров (не коммитим)
|
||||
**/.terraform/
|
||||
**/.terraform.lock.hcl
|
||||
**/terraform.tfstate
|
||||
**/terraform.tfstate.backup
|
||||
**/handler.zip
|
||||
|
||||
@@ -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`.
|
||||
|
||||
+1
-1
@@ -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 тестирования |
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -16,7 +16,7 @@ terraform {
|
||||
required_providers {
|
||||
sless = {
|
||||
source = "terra.k8c.ru/naeel/sless"
|
||||
version = "~> 0.1"
|
||||
version = "~> 0.1.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
Reference in New Issue
Block a user