diff --git a/TODO.txt b/TODO.txt new file mode 100644 index 0000000..e224727 --- /dev/null +++ b/TODO.txt @@ -0,0 +1,20 @@ +MQ-триггер (MessageQueue) — запуск при появлении сообщения в очереди (Kafka, NATS, RabbitMQ и т.д.) +Kube-Watch триггер — запуск при изменении объектов в Kubernetes - а это разве может быть реализовано ? + +В Fission — да, это штатные встроенные типы триггеров. Fission из коробки поддерживает MQ (Kafka, NATS, Azure Queue) и Kube-Watch через отдельные компоненты (kubewatcher, mqtrigger). + +Но в вашей консоли это не реализовано — нет UI для создания/управления этими триггерами. Только HTTP и Cron. Если потребуется — можно добавить. + +от что реально полезно и не очень сложно: + +Для пользователя: + +История вызовов — последние N вызовов функции: время, статус, время ответа. Сейчас вызвал и забыл. +Метрики — счётчик вызовов, средний latency, % ошибок. У вас уже есть metrics-collector — данные есть, нужен только UI. +Автообновление логов — сейчас нужно жать "Обновить" вручную. Можно автополлинг каждые 3-5 сек. +Копировать маршрут одной кнопкой — иконка 📋 рядом с маршрутом в таблице. +Статус функции — Ready/NotReady/Error прямо в таблице (сейчас не видно). +Для управления: +6. Клонировать функцию — создать копию с новым именем, сохранив код и настройки. +7. Экспорт/импорт — скачать функцию как zip, загрузить обратно. +8. Переменные окружения — env vars для функции через UI (сейчас нельзя задать). \ No newline at end of file diff --git a/console/deploy/console.yaml b/console/deploy/console.yaml index b0a4418..8838d93 100644 --- a/console/deploy/console.yaml +++ b/console/deploy/console.yaml @@ -55,7 +55,7 @@ spec: serviceAccountName: fission-console containers: - name: console - image: naeel/fission-console:v1.3.69 + image: naeel/fission-console:v1.3.70 imagePullPolicy: Always ports: - containerPort: 8090 diff --git a/console/internal/api/handlers.go b/console/internal/api/handlers.go index 8bf7f9b..c2e53e7 100644 --- a/console/internal/api/handlers.go +++ b/console/internal/api/handlers.go @@ -51,11 +51,11 @@ const fissionSourceTypeAnnotation = "fission-console/source-type" const defaultFunctionInvokeTimeout = 60 * time.Second // handleFunctionsRoot обрабатывает запросы к /console/api/functions без имени функции. -// GET → список всех функций, POST → создать новую. +// GET → список всех функций со статусами, POST → создать новую. func (s *Server) handleFunctionsRoot(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: - s.handleList(fission.FunctionGVR)(w, r) + s.handleListFunctionsWithStatus(w, r) case http.MethodPost: s.handleCreateFunction(w, r) default: diff --git a/console/internal/api/status.go b/console/internal/api/status.go new file mode 100644 index 0000000..ebb5e34 --- /dev/null +++ b/console/internal/api/status.go @@ -0,0 +1,135 @@ +// Package api — определение статуса функции. +// +// Этот файл содержит функции для определения статуса функции на основе: +// - buildStatus пакета (Package CRD) +// - состояния pod-а (kubernetes Pod) +// +// Возможные статусы: +// - "Ready" (зелёный) — Pod в состоянии Running +// - "Cold" (синий) — функция готова, но нет активного Pod +// - "Building" (жёлтый) — Package ещё собирается (buildStatus != "succeeded") +// - "Error" (красный) — Pod в состоянии ошибки (CrashLoopBackOff, ImagePullBackOff и т.д.) или build failed +package api + +import ( + "context" + "fmt" + "net/http" + "time" + + "fission-console/internal/fission" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +// FunctionStatus — расширенный объект Function с полем status. +type FunctionStatus struct { + // Встраиваем исходный объект и добавляем status + Object map[string]any `json:"object,inline"` + Status string `json:"status"` // "Ready", "Cold", "Building", "Error" +} + +// getFunctionStatus определяет статус функции на основе pod-ов и Package. +func (s *Server) getFunctionStatus(ctx context.Context, ns string, fn *unstructured.Unstructured) (string, error) { + fnName := fn.GetName() + + // Шаг 1: проверяем buildStatus пакета (если есть) + packageName, _, _ := unstructured.NestedString(fn.Object, "spec", "package", "packageref", "name") + if packageName != "" { + pkg, pkgErr := s.dyn.Resource(fission.PackageGVR).Namespace(ns).Get(ctx, packageName, metav1.GetOptions{}) + if pkgErr == nil { + buildStatus, _, _ := unstructured.NestedString(pkg.Object, "status", "buildstatus") + if buildStatus == "failed" { + return "Error", nil + } + if buildStatus != "succeeded" && buildStatus != "" { + // Building, Pending и т.д. + return "Building", nil + } + } + } + + // Шаг 2: ищем Pod-ы для этой функции (по лейблу functionName=) + pods, err := s.kube.CoreV1().Pods(ns).List(ctx, metav1.ListOptions{ + LabelSelector: fmt.Sprintf("functionName=%s", fnName), + }) + if err != nil && !apierrors.IsNotFound(err) { + // Логируем ошибку, но не падаем — возвращаем "Cold" (функция есть, но pod-а нет) + return "Cold", nil + } + + // Шаг 3: анализируем состояние pod-ов + if pods != nil && len(pods.Items) > 0 { + for _, pod := range pods.Items { + // Проверяем фазу pod-а + phase := pod.Status.Phase + if phase == "Running" { + // Дополнительная проверка: все контейнеры готовы? + allReady := true + for _, status := range pod.Status.ContainerStatuses { + if !status.Ready { + allReady = false + break + } + } + if allReady { + return "Ready", nil + } + } + + // Проверяем состояние контейнеров (CrashLoopBackOff, ImagePullBackOff и т.д.) + for _, status := range pod.Status.ContainerStatuses { + if status.State.Waiting != nil && status.State.Waiting.Reason == "CrashLoopBackOff" { + return "Error", nil + } + if status.State.Waiting != nil && status.State.Waiting.Reason == "ImagePullBackOff" { + return "Error", nil + } + if status.LastTerminationState.Terminated != nil && status.LastTerminationState.Terminated.ExitCode != 0 { + return "Error", nil + } + } + } + + // Pod есть, но не в Running или Error состоянии → Building/Deploying + return "Building", nil + } + + // Нет pod-ов → Cold (функция холодная, но готова) + return "Cold", nil +} + +// handleListFunctionsWithStatus возвращает список функций с полем "status". +// Добавляет status в поле .metadata.annotations["fission-console/status"] каждой функции. +func (s *Server) handleListFunctionsWithStatus(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second) // 30 сек для определения статуса всех функций + defer cancel() + ns := s.userNS(r) + + // Получаем список функций + list, err := s.dyn.Resource(fission.FunctionGVR).Namespace(ns).List(ctx, metav1.ListOptions{}) + if err != nil { + writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("list functions: %v", err)) + return + } + + // Обогащаем каждую функцию статусом (добавляем в .metadata.annotations["fission-console/status"]) + for i := range list.Items { + status, _ := s.getFunctionStatus(ctx, ns, &list.Items[i]) + anns := list.Items[i].GetAnnotations() + if anns == nil { + anns = make(map[string]string) + } + anns["fission-console/status"] = status + list.Items[i].SetAnnotations(anns) + } + + writeJSON(w, http.StatusOK, list.Items) +} diff --git a/console/ui/index.html b/console/ui/index.html index 88cc3b4..f954405 100644 --- a/console/ui/index.html +++ b/console/ui/index.html @@ -102,7 +102,7 @@
NUBES
FISSION CONSOLE
-
v1.3.69
+
v1.3.70
@@ -142,10 +142,23 @@
Функции
Actions: view, edit code, invoke, delete
+ + +
+ Статусы функций: + + Готова + Холодная + Сборка + Ошибка + +
+ + @@ -485,7 +498,7 @@
- v1.3.69 + v1.3.70
diff --git a/console/ui/js/app.js b/console/ui/js/app.js index 6820a0c..3c7a855 100644 --- a/console/ui/js/app.js +++ b/console/ui/js/app.js @@ -48,6 +48,20 @@ async function reloadAll() { const createdAt = ann['fission-console/created-at'] || meta.creationTimestamp || '-'; const updatedAt = ann['fission-console/updated-at'] || createdAt; const sourceType = ann['fission-console/source-type'] || 'code'; + const status = ann['fission-console/status'] || 'Cold'; + + // Статус функции: иконка и цвет + const statusColor = { + 'Ready': '#2a2', 'Cold': '#22a', 'Building': '#aa2', 'Error': '#a22' + }[status] || '#666'; + const statusTitle = { + 'Ready': 'Готова (есть pod)', + 'Cold': 'Холодная (pod создаётся при первом вызове)', + 'Building': 'Собирается / деплоится', + 'Error': 'Ошибка (pod в CrashLoopBackOff или build failed)' + }[status] || 'Неизвестно'; + const statusDot = ''; + const sourceIcon = sourceType === 'archive' ? '📦' : '📝'; @@ -58,7 +72,7 @@ async function reloadAll() { var infoData = h(JSON.stringify({ name: name, env: env, pkg: pkg, entrypoint: entrypoint, timeout: timeout, route: route, methods: methods, sourceType: sourceType, - createdAt: createdAt, updatedAt: updatedAt, cron: cron + createdAt: createdAt, updatedAt: updatedAt, cron: cron, status: status })); var actions = tfBadge + ' ' + @@ -68,6 +82,7 @@ async function reloadAll() { ''; return '' + '' + + '' + '' + '' + '' +
ИмяСтатус Тип Создана Изменена
' + h(name) + '' + statusDot + '' + sourceIcon + '' + timestampCell(createdAt) + '' + timestampCell(updatedAt) + '