Files
sless/internal/api/handler/triggers.go
T
“Naeel” a1774e178f refactor: SoC — EnsureNamespace в namespace.go, маршрут /ensure, client.EnsureNamespace, fix secrets в .gitignore
- handler.go: убраны бизнес-логика и k8s-типы (corev1/k8serrors/metav1)
  handler.go теперь только инфраструктура: Handler struct + helpers
- namespace.go: новый файл — EnsureNamespace хендлер живёт здесь
  SoC: создание namespace — отдельная ответственность, не смешивается с CRUD
- router.go: добавлен маршрут POST /v1/namespaces/{namespace}/ensure
- client.go: добавлен метод EnsureNamespace(ctx, ns) → POST /ensure
- provider.go: Configure() вызывает c.EnsureNamespace(ctx, namespace) после создания Client
  Namespace создаётся ОДИН РАЗ при инициализации провайдера
  Resource-хендлеры (Function, Trigger, Job) namespace не трогают
- .gitignore: добавлена директория secrets/ (токены, ключи)
- provider v0.1.13, operator v0.1.21

Operator: naeel/sless-operator:v0.1.21
Provider: terra.k8c.ru/naeel/sless v0.1.13
2026-03-11 08:37:33 +04:00

196 lines
6.7 KiB
Go

// Изменено: 2026-03-08
// 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) {
w.WriteHeader(http.StatusNoContent)
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))
}