// Изменено: 2026-03-08 // Изменено: 2026-03-21 (fix: DeleteTrigger возвращает 404 вместо 204 при отсутствующем объекте) // triggers.go — CRUD handlers для Trigger CRD. // Триггеры привязаны к Function через FunctionRef. // Namespace берётся из URL: /v1/namespaces/{namespace}/triggers/{name} package handler import ( "encoding/json" "net/http" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" slessv1alpha1 "gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/api/v1alpha1" ) // triggerRequest — тело запроса для создания/обновления триггера. type triggerRequest struct { Name string `json:"name"` Type string `json:"type"` // http | cron FunctionRef string `json:"function"` // имя Function CRD Schedule string `json:"schedule"` // cron-расписание, только для type=cron PreWarmSeconds int32 `json:"pre_warm_seconds"` // Enabled — по умолчанию true (включён). false = Deployment масштабируется до 0. // Используем *bool чтобы различать nil (не передан) от false (явно выключен). Enabled *bool `json:"enabled"` } // triggerResponse — ответ при чтении триггера. type triggerResponse struct { Name string `json:"name"` Namespace string `json:"namespace"` Type string `json:"type"` FunctionRef string `json:"function"` Schedule string `json:"schedule,omitempty"` Enabled bool `json:"enabled"` Active bool `json:"active"` URL string `json:"url,omitempty"` Message string `json:"message,omitempty"` } // trToResponse конвертирует Trigger CRD в ответ API. func trToResponse(tr *slessv1alpha1.Trigger) triggerResponse { return triggerResponse{ Name: tr.Name, Namespace: tr.Namespace, Type: string(tr.Spec.Type), FunctionRef: tr.Spec.FunctionRef, Schedule: tr.Spec.Schedule, Enabled: tr.Spec.Enabled, Active: tr.Status.Active, URL: tr.Status.URL, Message: tr.Status.Message, } } // ListTriggers — GET /v1/namespaces/{namespace}/triggers func (h *Handler) ListTriggers(w http.ResponseWriter, r *http.Request) { ns := namespace(r) list := &slessv1alpha1.TriggerList{} if err := h.K8s.List(r.Context(), list, client.InNamespace(ns)); err != nil { writeJSON(w, http.StatusInternalServerError, errResp(err.Error())) return } result := make([]triggerResponse, 0, len(list.Items)) for i := range list.Items { result = append(result, trToResponse(&list.Items[i])) } writeJSON(w, http.StatusOK, result) } // CreateTrigger — POST /v1/namespaces/{namespace}/triggers func (h *Handler) CreateTrigger(w http.ResponseWriter, r *http.Request) { ns := namespace(r) var req triggerRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, errResp("invalid JSON: "+err.Error())) return } if req.Name == "" || req.Type == "" || req.FunctionRef == "" { writeJSON(w, http.StatusBadRequest, errResp("name, type and function are required")) return } if req.Type != "http" && req.Type != "cron" { writeJSON(w, http.StatusBadRequest, errResp("type must be \"http\" or \"cron\"")) return } if req.Type == "cron" && req.Schedule == "" { writeJSON(w, http.StatusBadRequest, errResp("schedule is required for cron trigger")) return } tr := &slessv1alpha1.Trigger{ ObjectMeta: metav1.ObjectMeta{ Name: req.Name, Namespace: ns, }, Spec: slessv1alpha1.TriggerSpec{ Type: slessv1alpha1.TriggerType(req.Type), FunctionRef: req.FunctionRef, Schedule: req.Schedule, PreWarmSeconds: req.PreWarmSeconds, // По умолчанию enabled=true, если явно не передано false Enabled: req.Enabled == nil || *req.Enabled, }, } if err := h.K8s.Create(r.Context(), tr); err != nil { if errors.IsAlreadyExists(err) { writeJSON(w, http.StatusConflict, errResp("trigger already exists")) return } writeJSON(w, http.StatusInternalServerError, errResp(err.Error())) return } writeJSON(w, http.StatusCreated, trToResponse(tr)) } // GetTrigger — GET /v1/namespaces/{namespace}/triggers/{name} func (h *Handler) GetTrigger(w http.ResponseWriter, r *http.Request) { ns := namespace(r) name := pathVar(r, "name") tr := &slessv1alpha1.Trigger{} if err := h.K8s.Get(r.Context(), client.ObjectKey{Name: name, Namespace: ns}, tr); err != nil { if errors.IsNotFound(err) { writeJSON(w, http.StatusNotFound, errResp("trigger not found")) return } writeJSON(w, http.StatusInternalServerError, errResp(err.Error())) return } writeJSON(w, http.StatusOK, trToResponse(tr)) } // DeleteTrigger — DELETE /v1/namespaces/{namespace}/triggers/{name} func (h *Handler) DeleteTrigger(w http.ResponseWriter, r *http.Request) { ns := namespace(r) name := pathVar(r, "name") tr := &slessv1alpha1.Trigger{} if err := h.K8s.Get(r.Context(), client.ObjectKey{Name: name, Namespace: ns}, tr); err != nil { if errors.IsNotFound(err) { writeJSON(w, http.StatusNotFound, errResp("trigger not found")) return } writeJSON(w, http.StatusInternalServerError, errResp(err.Error())) return } if err := h.K8s.Delete(r.Context(), tr); err != nil { writeJSON(w, http.StatusInternalServerError, errResp(err.Error())) return } w.WriteHeader(http.StatusNoContent) } // UpdateTrigger — PATCH /v1/namespaces/{namespace}/triggers/{name} // Позволяет изменить поля триггера без пересоздания (в частности enabled). // Принимает частичный JSON: только переданные поля обновляются. func (h *Handler) UpdateTrigger(w http.ResponseWriter, r *http.Request) { ns := namespace(r) name := pathVar(r, "name") var req triggerRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, errResp("invalid JSON: "+err.Error())) return } tr := &slessv1alpha1.Trigger{} if err := h.K8s.Get(r.Context(), client.ObjectKey{Name: name, Namespace: ns}, tr); err != nil { if errors.IsNotFound(err) { writeJSON(w, http.StatusNotFound, errResp("trigger not found")) return } writeJSON(w, http.StatusInternalServerError, errResp(err.Error())) return } // Обновляем только явно переданные поля if req.Enabled != nil { tr.Spec.Enabled = *req.Enabled } if req.Schedule != "" { tr.Spec.Schedule = req.Schedule } if req.PreWarmSeconds != 0 { tr.Spec.PreWarmSeconds = req.PreWarmSeconds } if err := h.K8s.Update(r.Context(), tr); err != nil { writeJSON(w, http.StatusInternalServerError, errResp(err.Error())) return } writeJSON(w, http.StatusOK, trToResponse(tr)) }