- 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
212 lines
7.1 KiB
Go
212 lines
7.1 KiB
Go
// Изменено: 2026-03-07
|
|
// functions.go — CRUD handlers для Function CRD.
|
|
// Принимает JSON, создаёт/обновляет/удаляет k8s ресурсы Function.
|
|
// Namespace берётся из URL: /v1/namespaces/{namespace}/functions/{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"
|
|
)
|
|
|
|
// functionRequest — тело запроса для создания/обновления функции.
|
|
type functionRequest struct {
|
|
Name string `json:"name"`
|
|
Runtime string `json:"runtime"`
|
|
Entrypoint string `json:"entrypoint"`
|
|
MemoryMB int32 `json:"memory_mb"`
|
|
TimeoutSec int32 `json:"timeout_sec"`
|
|
Env map[string]string `json:"env_vars"`
|
|
S3Bucket string `json:"s3_bucket"`
|
|
S3Key string `json:"s3_key"`
|
|
}
|
|
|
|
// functionResponse — ответ при чтении функции.
|
|
type functionResponse struct {
|
|
Name string `json:"name"`
|
|
Namespace string `json:"namespace"`
|
|
Runtime string `json:"runtime"`
|
|
Entrypoint string `json:"entrypoint"`
|
|
MemoryMB int32 `json:"memory_mb"`
|
|
TimeoutSec int32 `json:"timeout_sec"`
|
|
Env map[string]string `json:"env_vars"`
|
|
S3Bucket string `json:"s3_bucket"`
|
|
S3Key string `json:"s3_key"`
|
|
Phase slessv1alpha1.FunctionPhase `json:"phase"`
|
|
ImageRef string `json:"image_ref"`
|
|
Message string `json:"message,omitempty"`
|
|
}
|
|
|
|
// fnToResponse конвертирует CRD в ответ API.
|
|
func fnToResponse(fn *slessv1alpha1.Function) functionResponse {
|
|
return functionResponse{
|
|
Name: fn.Name,
|
|
Namespace: fn.Namespace,
|
|
Runtime: fn.Spec.Runtime,
|
|
Entrypoint: fn.Spec.Entrypoint,
|
|
MemoryMB: fn.Spec.MemoryMB,
|
|
TimeoutSec: fn.Spec.TimeoutSec,
|
|
Env: fn.Spec.Env,
|
|
S3Bucket: fn.Spec.S3Bucket,
|
|
S3Key: fn.Spec.S3Key,
|
|
Phase: fn.Status.Phase,
|
|
ImageRef: fn.Status.ImageRef,
|
|
Message: fn.Status.Message,
|
|
}
|
|
}
|
|
|
|
// ListFunctions — GET /v1/namespaces/{namespace}/functions
|
|
func (h *Handler) ListFunctions(w http.ResponseWriter, r *http.Request) {
|
|
ns := namespace(r)
|
|
list := &slessv1alpha1.FunctionList{}
|
|
if err := h.K8s.List(r.Context(), list, client.InNamespace(ns)); err != nil {
|
|
writeJSON(w, http.StatusInternalServerError, errResp(err.Error()))
|
|
return
|
|
}
|
|
result := make([]functionResponse, 0, len(list.Items))
|
|
for i := range list.Items {
|
|
result = append(result, fnToResponse(&list.Items[i]))
|
|
}
|
|
writeJSON(w, http.StatusOK, result)
|
|
}
|
|
|
|
// CreateFunction — POST /v1/namespaces/{namespace}/functions
|
|
func (h *Handler) CreateFunction(w http.ResponseWriter, r *http.Request) {
|
|
ns := namespace(r)
|
|
var req functionRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, errResp("invalid JSON: "+err.Error()))
|
|
return
|
|
}
|
|
if req.Name == "" || req.Runtime == "" {
|
|
writeJSON(w, http.StatusBadRequest, errResp("name and runtime are required"))
|
|
return
|
|
}
|
|
if req.Entrypoint == "" {
|
|
writeJSON(w, http.StatusBadRequest, errResp("entrypoint is required"))
|
|
return
|
|
}
|
|
if req.MemoryMB <= 0 || req.MemoryMB > 4096 {
|
|
writeJSON(w, http.StatusBadRequest, errResp("memory_mb must be between 1 and 4096"))
|
|
return
|
|
}
|
|
|
|
fn := &slessv1alpha1.Function{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: req.Name,
|
|
Namespace: ns,
|
|
},
|
|
Spec: slessv1alpha1.FunctionSpec{
|
|
Runtime: req.Runtime,
|
|
Entrypoint: req.Entrypoint,
|
|
MemoryMB: req.MemoryMB,
|
|
TimeoutSec: req.TimeoutSec,
|
|
Env: req.Env,
|
|
S3Bucket: req.S3Bucket,
|
|
S3Key: req.S3Key,
|
|
},
|
|
}
|
|
if err := h.K8s.Create(r.Context(), fn); err != nil {
|
|
if errors.IsAlreadyExists(err) {
|
|
writeJSON(w, http.StatusConflict, errResp("function already exists"))
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusInternalServerError, errResp(err.Error()))
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, fnToResponse(fn))
|
|
}
|
|
|
|
// GetFunction — GET /v1/namespaces/{namespace}/functions/{name}
|
|
func (h *Handler) GetFunction(w http.ResponseWriter, r *http.Request) {
|
|
ns := namespace(r)
|
|
name := pathVar(r, "name")
|
|
fn := &slessv1alpha1.Function{}
|
|
if err := h.K8s.Get(r.Context(), client.ObjectKey{Name: name, Namespace: ns}, fn); err != nil {
|
|
if errors.IsNotFound(err) {
|
|
writeJSON(w, http.StatusNotFound, errResp("function not found"))
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusInternalServerError, errResp(err.Error()))
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, fnToResponse(fn))
|
|
}
|
|
|
|
// UpdateFunction — PUT /v1/namespaces/{namespace}/functions/{name}
|
|
func (h *Handler) UpdateFunction(w http.ResponseWriter, r *http.Request) {
|
|
ns := namespace(r)
|
|
name := pathVar(r, "name")
|
|
var req functionRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, errResp("invalid JSON: "+err.Error()))
|
|
return
|
|
}
|
|
|
|
fn := &slessv1alpha1.Function{}
|
|
if err := h.K8s.Get(r.Context(), client.ObjectKey{Name: name, Namespace: ns}, fn); err != nil {
|
|
if errors.IsNotFound(err) {
|
|
writeJSON(w, http.StatusNotFound, errResp("function not found"))
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusInternalServerError, errResp(err.Error()))
|
|
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
|
|
fn.Spec.Runtime = req.Runtime
|
|
fn.Spec.Entrypoint = req.Entrypoint
|
|
fn.Spec.MemoryMB = req.MemoryMB
|
|
fn.Spec.TimeoutSec = req.TimeoutSec
|
|
fn.Spec.Env = req.Env
|
|
if req.S3Bucket != "" {
|
|
fn.Spec.S3Bucket = req.S3Bucket
|
|
}
|
|
if req.S3Key != "" {
|
|
fn.Spec.S3Key = req.S3Key
|
|
}
|
|
|
|
if err := h.K8s.Update(r.Context(), fn); err != nil {
|
|
writeJSON(w, http.StatusInternalServerError, errResp(err.Error()))
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, fnToResponse(fn))
|
|
}
|
|
|
|
// DeleteFunction — DELETE /v1/namespaces/{namespace}/functions/{name}
|
|
func (h *Handler) DeleteFunction(w http.ResponseWriter, r *http.Request) {
|
|
ns := namespace(r)
|
|
name := pathVar(r, "name")
|
|
fn := &slessv1alpha1.Function{}
|
|
if err := h.K8s.Get(r.Context(), client.ObjectKey{Name: name, Namespace: ns}, fn); 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(), fn); err != nil {
|
|
writeJSON(w, http.StatusInternalServerError, errResp(err.Error()))
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|