- sqs-consumer: new Go binary v1.2 with tokenManager (auto-login /auth/login, 5min cache, retry on 401) - console: add MQ/KW triggers UI (mqtriggers.go, kwtriggers.go, mq.js) - console: update python-env image to v1.1 (boto3+psycopg2+requests) - weather-demo: fix consumer/main.py (Flask Request.get_json instead of dict access) - weather-demo: fix fetcher/main.py (boto3 SQS publish, 5 cities) - weather-demo: update main.tf (python-env v1.1, deploy_type=literal) - python-env: add psycopg2-binary to Dockerfile (v1.1) - terraform provider: client auth fix
242 lines
7.8 KiB
Go
242 lines
7.8 KiB
Go
package api
|
|
|
|
// kwtriggers.go — CRUD хендлеры для KubernetesWatchTrigger (Fission KW Trigger).
|
|
//
|
|
// РЕШЕНИЕ ПО АРХИТЕКТУРЕ (2026-05-11):
|
|
// KubernetesWatchTrigger позволяет вызывать функцию при изменении K8s объектов.
|
|
// spec.type — тип ресурса: Pod, Service, Deployment, ConfigMap, и т.д.
|
|
// spec.namespace — namespace для слежения (по умолчанию = namespace пользователя)
|
|
// spec.labelselector — label selector в формате "key=value,key2=value2"
|
|
// spec.functionref — ссылка на функцию
|
|
//
|
|
// ОСОБЕННОСТИ:
|
|
// - Fission kubewatcher компонент должен быть задеплоен.
|
|
// - namespace в spec — это WATCHED namespace (не namespace триггера).
|
|
// Для безопасности ограничиваем: только namespace пользователя или пустое (тогда = userNS).
|
|
// - labelselector опционален, "" = смотрим на все ресурсы типа resourceType в namespace.
|
|
//
|
|
// ОШИБКИ В ПРОЦЕССЕ:
|
|
// - (нет, первая реализация)
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"fission-console/internal/fission"
|
|
"fission-console/internal/model"
|
|
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
)
|
|
|
|
// validKWResourceTypes — поддерживаемые типы ресурсов для слежения.
|
|
// Расширяемо — это не ограничение CRD, просто UI-валидация.
|
|
var validKWResourceTypes = map[string]struct{}{
|
|
"pod": {},
|
|
"service": {},
|
|
"deployment": {},
|
|
"configmap": {},
|
|
"secret": {},
|
|
"namespace": {},
|
|
"replicaset": {},
|
|
"statefulset": {},
|
|
"daemonset": {},
|
|
"job": {},
|
|
}
|
|
|
|
func (s *Server) handleKWTriggersRoot(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
s.handleList(fission.KWTrigGVR)(w, r)
|
|
case http.MethodPost:
|
|
s.handleCreateKWTrigger(w, r)
|
|
default:
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleKWTriggersAction(w http.ResponseWriter, r *http.Request) {
|
|
path := strings.TrimPrefix(r.URL.Path, "/console/api/kwtriggers/")
|
|
path = strings.TrimPrefix(path, "/api/kwtriggers/")
|
|
name := strings.Trim(path, "/")
|
|
if name == "" || strings.Contains(name, "/") {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
s.handleGetKWTrigger(w, r, name)
|
|
case http.MethodDelete:
|
|
s.handleDeleteKWTrigger(w, r, name)
|
|
default:
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleCreateKWTrigger(w http.ResponseWriter, r *http.Request) {
|
|
var req model.CreateKWTriggerRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("decode request: %v", err))
|
|
return
|
|
}
|
|
|
|
ns := s.userNS(r)
|
|
|
|
// Если namespace не задан — используем namespace пользователя
|
|
if strings.TrimSpace(req.Namespace) == "" {
|
|
req.Namespace = ns
|
|
}
|
|
|
|
// Безопасность: нельзя смотреть за чужим namespace
|
|
if req.Namespace != ns {
|
|
writeJSONError(w, http.StatusForbidden, "can only watch your own namespace")
|
|
return
|
|
}
|
|
|
|
if err := validateKWTriggerRequest(req); err != nil {
|
|
writeJSONError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
|
|
defer cancel()
|
|
|
|
// Проверяем что функция существует
|
|
if _, err := s.dyn.Resource(fission.FunctionGVR).Namespace(ns).Get(ctx, req.FunctionName, metav1.GetOptions{}); err != nil {
|
|
if apierrors.IsNotFound(err) {
|
|
writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("function %q not found", req.FunctionName))
|
|
return
|
|
}
|
|
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("get function: %v", err))
|
|
return
|
|
}
|
|
|
|
obj := buildKWTriggerObject(ns, req)
|
|
created, err := s.dyn.Resource(fission.KWTrigGVR).Namespace(ns).Create(ctx, obj, metav1.CreateOptions{})
|
|
if err != nil {
|
|
if apierrors.IsAlreadyExists(err) {
|
|
writeJSONError(w, http.StatusConflict, fmt.Sprintf("kwtrigger %q already exists", req.Name))
|
|
return
|
|
}
|
|
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("create kwtrigger: %v", err))
|
|
return
|
|
}
|
|
|
|
writeAnyJSON(w, http.StatusCreated, kwTriggerResponse(created))
|
|
}
|
|
|
|
func (s *Server) handleGetKWTrigger(w http.ResponseWriter, r *http.Request, name string) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
obj, err := s.dyn.Resource(fission.KWTrigGVR).Namespace(s.userNS(r)).Get(ctx, name, metav1.GetOptions{})
|
|
if err != nil {
|
|
status := http.StatusBadGateway
|
|
if apierrors.IsNotFound(err) {
|
|
status = http.StatusNotFound
|
|
}
|
|
writeJSONError(w, status, fmt.Sprintf("get kwtrigger %q: %v", name, err))
|
|
return
|
|
}
|
|
|
|
writeAnyJSON(w, http.StatusOK, kwTriggerResponse(obj))
|
|
}
|
|
|
|
func (s *Server) handleDeleteKWTrigger(w http.ResponseWriter, r *http.Request, name string) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
if err := s.dyn.Resource(fission.KWTrigGVR).Namespace(s.userNS(r)).Delete(ctx, name, metav1.DeleteOptions{}); err != nil {
|
|
status := http.StatusBadGateway
|
|
if apierrors.IsNotFound(err) {
|
|
status = http.StatusNotFound
|
|
}
|
|
writeJSONError(w, status, fmt.Sprintf("delete kwtrigger %q: %v", name, err))
|
|
return
|
|
}
|
|
|
|
writeAnyJSON(w, http.StatusOK, map[string]any{"deleted": true, "name": name})
|
|
}
|
|
|
|
// --- Вспомогательные функции ---
|
|
|
|
func validateKWTriggerRequest(req model.CreateKWTriggerRequest) error {
|
|
if strings.TrimSpace(req.Name) == "" {
|
|
return fmt.Errorf("name is required")
|
|
}
|
|
if strings.TrimSpace(req.FunctionName) == "" {
|
|
return fmt.Errorf("functionName is required")
|
|
}
|
|
rt := strings.ToLower(strings.TrimSpace(req.ResourceType))
|
|
if _, ok := validKWResourceTypes[rt]; !ok {
|
|
return fmt.Errorf("resourceType must be one of: Pod, Service, Deployment, ConfigMap, Secret, Namespace, ReplicaSet, StatefulSet, DaemonSet, Job")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func buildKWTriggerObject(ns string, req model.CreateKWTriggerRequest) *unstructured.Unstructured {
|
|
// Fission ожидает capitalize: Pod, Service, Deployment
|
|
resourceType := capitalize(strings.TrimSpace(req.ResourceType))
|
|
|
|
spec := map[string]any{
|
|
"type": resourceType,
|
|
"namespace": req.Namespace,
|
|
"functionref": map[string]any{
|
|
"type": "name",
|
|
"name": req.FunctionName,
|
|
},
|
|
}
|
|
if req.LabelSelector != "" {
|
|
spec["labelselector"] = req.LabelSelector
|
|
}
|
|
|
|
return &unstructured.Unstructured{Object: map[string]any{
|
|
"apiVersion": "fission.io/v1",
|
|
"kind": "KubernetesWatchTrigger",
|
|
"metadata": map[string]any{
|
|
"name": req.Name,
|
|
"namespace": ns,
|
|
},
|
|
"spec": spec,
|
|
}}
|
|
}
|
|
|
|
func kwTriggerResponse(obj *unstructured.Unstructured) map[string]any {
|
|
spec, _ := obj.Object["spec"].(map[string]any)
|
|
if spec == nil {
|
|
spec = map[string]any{}
|
|
}
|
|
fnref, _ := spec["functionref"].(map[string]any)
|
|
fnName := ""
|
|
if fnref != nil {
|
|
fnName, _ = fnref["name"].(string)
|
|
}
|
|
return map[string]any{
|
|
"metadata": map[string]any{
|
|
"name": obj.GetName(),
|
|
"namespace": obj.GetNamespace(),
|
|
},
|
|
"spec": map[string]any{
|
|
"resourceType": spec["type"],
|
|
"namespace": spec["namespace"],
|
|
"labelSelector": spec["labelselector"],
|
|
"functionName": fnName,
|
|
},
|
|
}
|
|
}
|
|
|
|
// capitalize приводит первый символ к верхнему регистру, остальное без изменений.
|
|
// "pod" → "Pod", "deployment" → "Deployment"
|
|
func capitalize(s string) string {
|
|
if s == "" {
|
|
return s
|
|
}
|
|
return strings.ToUpper(s[:1]) + strings.ToLower(s[1:])
|
|
}
|