fix: stage0 quick fixes (operator v0.1.19)
- 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
This commit is contained in:
@@ -91,8 +91,8 @@ func (r *FunctionJobReconciler) Reconcile(ctx context.Context, req ctrl.Request)
|
|||||||
fj.Status.Phase = slessv1alpha1.FunctionJobPhasePending
|
fj.Status.Phase = slessv1alpha1.FunctionJobPhasePending
|
||||||
fj.Status.Message = "waiting for function Ready (current: " + string(fn.Status.Phase) + ")"
|
fj.Status.Message = "waiting for function Ready (current: " + string(fn.Status.Phase) + ")"
|
||||||
_ = r.Status().Update(ctx, fj)
|
_ = r.Status().Update(ctx, fj)
|
||||||
// Повторный reconcile придёт когда Function изменится
|
// RequeueAfter: опрашиваем каждые 15с пока Function не станет Ready.
|
||||||
return ctrl.Result{}, nil
|
return ctrl.Result{RequeueAfter: 15 * time.Second}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
deployNS := "sless-fn-" + fj.Namespace
|
deployNS := "sless-fn-" + fj.Namespace
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Изменено: 2026-03-08
|
// Изменено: 2026-03-10
|
||||||
// TriggerReconciler — контроллер триггеров.
|
// TriggerReconciler — контроллер триггеров.
|
||||||
// HTTP триггер: создаёт Service + Ingress в namespace функции.
|
// HTTP триггер: создаёт Service + Ingress в namespace функции.
|
||||||
// Cron триггер: создаёт k8s CronJob который периодически вызывает функцию по внутреннему URL.
|
// Cron триггер: создаёт k8s CronJob который периодически вызывает функцию по внутреннему URL.
|
||||||
@@ -8,6 +8,7 @@ package controllers
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
appsv1 "k8s.io/api/apps/v1"
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
batchv1 "k8s.io/api/batch/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.Active = false
|
||||||
tr.Status.Message = "waiting for function to be Ready (current: " + string(fn.Status.Phase) + ")"
|
tr.Status.Message = "waiting for function to be Ready (current: " + string(fn.Status.Phase) + ")"
|
||||||
_ = r.Status().Update(ctx, tr)
|
_ = r.Status().Update(ctx, tr)
|
||||||
// Повторный reconcile придёт когда Function изменится (watch ниже)
|
// RequeueAfter: опрашиваем каждые 15с пока Function не станет Ready.
|
||||||
return ctrl.Result{}, nil
|
// Watch на Function не добавлен, чтобы не усложнять контроллер на этом этапе.
|
||||||
|
return ctrl.Result{RequeueAfter: 15 * time.Second}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Управляем масштабом Deployment функции в зависимости от Enabled.
|
// Управляем масштабом Deployment функции в зависимости от Enabled.
|
||||||
@@ -308,8 +310,9 @@ func (r *TriggerReconciler) handleTriggerDeletion(ctx context.Context, tr *sless
|
|||||||
return ctrl.Result{}, nil
|
return ctrl.Result{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetupWithManager регистрирует контроллер и настраивает watch на Function.
|
// SetupWithManager регистрирует контроллер.
|
||||||
// Когда Function переходит в Ready — Trigger автоматически пересчитывается.
|
// Watch на Function не добавлен — вместо этого используется RequeueAfter 15s.
|
||||||
|
// Это упрощает код на текущем этапе; при росте нагрузки заменить на Watches.
|
||||||
func (r *TriggerReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
func (r *TriggerReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||||
return ctrl.NewControllerManagedBy(mgr).
|
return ctrl.NewControllerManagedBy(mgr).
|
||||||
For(&slessv1alpha1.Trigger{}).
|
For(&slessv1alpha1.Trigger{}).
|
||||||
|
|||||||
@@ -160,6 +160,16 @@ func (h *Handler) UpdateFunction(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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
|
// Обновляем только изменяемые поля spec
|
||||||
fn.Spec.Runtime = req.Runtime
|
fn.Spec.Runtime = req.Runtime
|
||||||
fn.Spec.Entrypoint = req.Entrypoint
|
fn.Spec.Entrypoint = req.Entrypoint
|
||||||
|
|||||||
@@ -1,61 +1,16 @@
|
|||||||
// Изменено: 2026-03-07
|
// Изменено: 2026-03-10
|
||||||
// invocations.go — GET-handlers для логов вызовов функций.
|
// invocations.go — stub для истории вызовов.
|
||||||
// Данные читаются из PostgreSQL (invocations таблица).
|
// Реальная запись вызовов не реализована — SaveInvocation нигде не вызывается.
|
||||||
// POST /invoke остаётся для будущего прямого вызова (v2).
|
// Endpoint возвращает 501 чтобы не вводить в заблуждение пустым списком.
|
||||||
|
// Реализовать когда появится реальный use case (биллинг, дебаг).
|
||||||
|
|
||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import "net/http"
|
||||||
"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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListInvocations — GET /v1/namespaces/{namespace}/functions/{name}/invocations
|
// 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) {
|
func (h *Handler) ListInvocations(w http.ResponseWriter, r *http.Request) {
|
||||||
ns := namespace(r)
|
writeJSON(w, http.StatusNotImplemented, errResp("invocation history not implemented yet"))
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,10 +34,6 @@ type Config struct {
|
|||||||
// Создаётся через hack/create-registry-secret.sh
|
// Создаётся через hack/create-registry-secret.sh
|
||||||
RegistrySecret string
|
RegistrySecret string
|
||||||
|
|
||||||
// FunctionNamespace — префикс namespace для функций пользователей.
|
|
||||||
// Итоговый namespace: FunctionNamespacePrefix + "-" + functionName
|
|
||||||
FunctionNamespacePrefix string
|
|
||||||
|
|
||||||
// BuilderImage — образ для сборки функций (kaniko или buildah)
|
// BuilderImage — образ для сборки функций (kaniko или buildah)
|
||||||
BuilderImage string
|
BuilderImage string
|
||||||
|
|
||||||
@@ -59,10 +55,9 @@ type Config struct {
|
|||||||
// Возвращает ошибку если обязательные переменные не заданы.
|
// Возвращает ошибку если обязательные переменные не заданы.
|
||||||
func Load() (*Config, error) {
|
func Load() (*Config, error) {
|
||||||
cfg := &Config{
|
cfg := &Config{
|
||||||
APIPort: 8080,
|
APIPort: 8080,
|
||||||
S3UseSSL: false,
|
S3UseSSL: false,
|
||||||
FunctionNamespacePrefix: "sless-fn",
|
BuilderImage: "gcr.io/kaniko-project/executor:latest",
|
||||||
BuilderImage: "gcr.io/kaniko-project/executor:latest",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Опциональный порт API
|
// Опциональный порт 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 != "" {
|
if v := os.Getenv("BUILDER_IMAGE"); v != "" {
|
||||||
cfg.BuilderImage = v
|
cfg.BuilderImage = v
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user