API (operator v0.1.13): - functions.go: добавлена валидация entrypoint (не пустой) и memory_mb (1-4096). Фиксирует БАГ-1/2/4 из негативных тестов. - triggers.go: добавлена валидация type (только 'http'/'cron'). Фиксирует БАГ-3 (неверное сообщение об ошибке). Провайдер (v0.1.7): - Добавлен пакет terraform-plugin-framework-validators v0.19.0 - function_resource: runtime OneOf, memory_mb 1-4096, timeout_sec 1-900 - trigger_resource: type OneOf(http, cron) - job_resource: run_id AtLeast(0) - examples/main.tf: обновлена версия до ~> 0.1.7 doc/errors/log.md: задокументированы исправления и результаты повторных тестов
196 lines
6.7 KiB
Go
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))
|
|
}
|