feat: REST API (gorilla/mux + slog) + trigger controller + main.go wiring

- controllers/trigger_controller.go: полная реализация, HTTP->Ingress+Service, Cron->CronJob
- internal/config: добавлены IngressHost, APIToken
- internal/api/router.go: gorilla/mux роутер /v1/namespaces/{ns}/...
- internal/api/handler/: functions, triggers, invocations, base handler с slog
- internal/api/middleware/: auth (Bearer token) + logging (slog)
- main.go: запуск operator + HTTP сервера параллельно
- go.sum: добавлен gorilla/mux v1.8.1
This commit is contained in:
“Naeel”
2026-03-07 08:46:10 +04:00
parent 5f25f61948
commit 43e7d0ea48
21 changed files with 1720 additions and 112 deletions
+143
View File
@@ -0,0 +1,143 @@
// Изменено: 2026-03-07
// 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"`
}
// 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"`
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,
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 == "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,
},
}
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)
}