From 408f58a9e25d6dddc13720b502abcc9289d6d3b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Tue, 10 Mar 2026 17:36:48 +0400 Subject: [PATCH] fix: stage0 quick fixes (operator v0.1.19) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TriggerReconciler: RequeueAfter 15s когда Function не Ready (ранее зависал без повторного reconcile) - FunctionJobReconciler: RequeueAfter 15s когда Function не Ready - UpdateFunction: добавлена валидация runtime/entrypoint/memory_mb (ранее мог затереть spec нулями при частичном обновлении) - CronJob: curlimages/curl:latest → curlimages/curl:8.5.0 (pin version) - Config: удалён FunctionNamespacePrefix (мёртвое поле, нигде не использовалось) - Invocations endpoint: возвращает 501 вместо пустого списка (SaveInvocation нигде не вызывается — честный ответ клиенту) - Собран образ naeel/sless-operator:v0.1.19 --- controllers/functionjob_controller.go | 4 +- controllers/trigger_controller.go | 13 +++--- internal/api/handler/functions.go | 10 +++++ internal/api/handler/invocations.go | 63 ++++----------------------- internal/config/config.go | 14 ++---- 5 files changed, 32 insertions(+), 72 deletions(-) diff --git a/controllers/functionjob_controller.go b/controllers/functionjob_controller.go index 953529b..97ced46 100644 --- a/controllers/functionjob_controller.go +++ b/controllers/functionjob_controller.go @@ -91,8 +91,8 @@ func (r *FunctionJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) fj.Status.Phase = slessv1alpha1.FunctionJobPhasePending fj.Status.Message = "waiting for function Ready (current: " + string(fn.Status.Phase) + ")" _ = r.Status().Update(ctx, fj) - // Повторный reconcile придёт когда Function изменится - return ctrl.Result{}, nil + // RequeueAfter: опрашиваем каждые 15с пока Function не станет Ready. + return ctrl.Result{RequeueAfter: 15 * time.Second}, nil } deployNS := "sless-fn-" + fj.Namespace diff --git a/controllers/trigger_controller.go b/controllers/trigger_controller.go index a50207b..77bb244 100644 --- a/controllers/trigger_controller.go +++ b/controllers/trigger_controller.go @@ -1,4 +1,4 @@ -// Изменено: 2026-03-08 +// Изменено: 2026-03-10 // TriggerReconciler — контроллер триггеров. // HTTP триггер: создаёт Service + Ingress в namespace функции. // Cron триггер: создаёт k8s CronJob который периодически вызывает функцию по внутреннему URL. @@ -8,6 +8,7 @@ package controllers import ( "context" "fmt" + "time" appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" @@ -81,8 +82,9 @@ func (r *TriggerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct tr.Status.Active = false tr.Status.Message = "waiting for function to be Ready (current: " + string(fn.Status.Phase) + ")" _ = r.Status().Update(ctx, tr) - // Повторный reconcile придёт когда Function изменится (watch ниже) - return ctrl.Result{}, nil + // RequeueAfter: опрашиваем каждые 15с пока Function не станет Ready. + // Watch на Function не добавлен, чтобы не усложнять контроллер на этом этапе. + return ctrl.Result{RequeueAfter: 15 * time.Second}, nil } // Управляем масштабом Deployment функции в зависимости от Enabled. @@ -308,8 +310,9 @@ func (r *TriggerReconciler) handleTriggerDeletion(ctx context.Context, tr *sless return ctrl.Result{}, nil } -// SetupWithManager регистрирует контроллер и настраивает watch на Function. -// Когда Function переходит в Ready — Trigger автоматически пересчитывается. +// SetupWithManager регистрирует контроллер. +// Watch на Function не добавлен — вместо этого используется RequeueAfter 15s. +// Это упрощает код на текущем этапе; при росте нагрузки заменить на Watches. func (r *TriggerReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&slessv1alpha1.Trigger{}). diff --git a/internal/api/handler/functions.go b/internal/api/handler/functions.go index 2921fbe..191edec 100644 --- a/internal/api/handler/functions.go +++ b/internal/api/handler/functions.go @@ -160,6 +160,16 @@ func (h *Handler) UpdateFunction(w http.ResponseWriter, r *http.Request) { return } + // Валидация обязательных полей — PUT семантика, нельзя обнулять критичные поля + if req.Runtime == "" || req.Entrypoint == "" { + writeJSON(w, http.StatusBadRequest, errResp("runtime and entrypoint are required")) + return + } + if req.MemoryMB <= 0 || req.MemoryMB > 4096 { + writeJSON(w, http.StatusBadRequest, errResp("memory_mb must be between 1 and 4096")) + return + } + // Обновляем только изменяемые поля spec fn.Spec.Runtime = req.Runtime fn.Spec.Entrypoint = req.Entrypoint diff --git a/internal/api/handler/invocations.go b/internal/api/handler/invocations.go index 369b7ac..d740443 100644 --- a/internal/api/handler/invocations.go +++ b/internal/api/handler/invocations.go @@ -1,61 +1,16 @@ -// Изменено: 2026-03-07 -// invocations.go — GET-handlers для логов вызовов функций. -// Данные читаются из PostgreSQL (invocations таблица). -// POST /invoke остаётся для будущего прямого вызова (v2). +// Изменено: 2026-03-10 +// invocations.go — stub для истории вызовов. +// Реальная запись вызовов не реализована — SaveInvocation нигде не вызывается. +// Endpoint возвращает 501 чтобы не вводить в заблуждение пустым списком. +// Реализовать когда появится реальный use case (биллинг, дебаг). package handler -import ( - "net/http" - "strconv" -) - -// invocationResponse — ответ при чтении записи вызова. -type invocationResponse struct { - ID string `json:"id"` - FunctionName string `json:"function_name"` - Namespace string `json:"namespace"` - Status string `json:"status"` - DurationMs int32 `json:"duration_ms"` - HTTPStatus *int32 `json:"http_status,omitempty"` // nil для cron триггеров - Logs string `json:"logs,omitempty"` - TriggerType string `json:"trigger_type"` - CreatedAt string `json:"created_at"` -} +import "net/http" // ListInvocations — GET /v1/namespaces/{namespace}/functions/{name}/invocations -// Возвращает последние limit вызовов (default 50, max 200). +// Пока не реализовано: SaveInvocation не вызывается нигде в коде. +// Возвращает 501 чтобы клиент знал что фича не реализована, а не просто нет данных. func (h *Handler) ListInvocations(w http.ResponseWriter, r *http.Request) { - ns := namespace(r) - fnName := pathVar(r, "name") - - limit := 50 - if q := r.URL.Query().Get("limit"); q != "" { - if v, err := strconv.Atoi(q); err == nil && v > 0 && v <= 200 { - limit = v - } - } - - rows, err := h.PG.ListInvocations(r.Context(), fnName, ns, limit) - if err != nil { - h.Log.Error("list invocations", "fn", fnName, "ns", ns, "err", err) - writeJSON(w, http.StatusInternalServerError, errResp(err.Error())) - return - } - - result := make([]invocationResponse, 0, len(rows)) - for _, row := range rows { - result = append(result, invocationResponse{ - ID: row.ID, - FunctionName: row.FunctionName, - Namespace: row.Namespace, - Status: row.Status, - DurationMs: row.DurationMs, - HTTPStatus: row.HTTPStatus, - Logs: row.Logs, - TriggerType: row.TriggerType, - CreatedAt: row.CreatedAt.Format("2006-01-02T15:04:05Z"), - }) - } - writeJSON(w, http.StatusOK, result) +writeJSON(w, http.StatusNotImplemented, errResp("invocation history not implemented yet")) } diff --git a/internal/config/config.go b/internal/config/config.go index d551d66..ef59f6a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -34,10 +34,6 @@ type Config struct { // Создаётся через hack/create-registry-secret.sh RegistrySecret string - // FunctionNamespace — префикс namespace для функций пользователей. - // Итоговый namespace: FunctionNamespacePrefix + "-" + functionName - FunctionNamespacePrefix string - // BuilderImage — образ для сборки функций (kaniko или buildah) BuilderImage string @@ -59,10 +55,9 @@ type Config struct { // Возвращает ошибку если обязательные переменные не заданы. func Load() (*Config, error) { cfg := &Config{ - APIPort: 8080, - S3UseSSL: false, - FunctionNamespacePrefix: "sless-fn", - BuilderImage: "gcr.io/kaniko-project/executor:latest", + APIPort: 8080, + S3UseSSL: false, + BuilderImage: "gcr.io/kaniko-project/executor:latest", } // Опциональный порт API @@ -115,9 +110,6 @@ func Load() (*Config, error) { } // Опциональные параметры с дефолтами - if v := os.Getenv("FUNCTION_NAMESPACE_PREFIX"); v != "" { - cfg.FunctionNamespacePrefix = v - } if v := os.Getenv("BUILDER_IMAGE"); v != "" { cfg.BuilderImage = v }