feat: функции со статусом (Ready/Cold/Building/Error)
- Backend: новый файл status.go с функцией определения статуса пода и Package buildStatus - API: GET /functions теперь возвращает функции с полем status в аннотациях - Frontend: добавлена колонка Status в таблицу функций с цветными кругами - Легенда статусов выше таблицы - Версия v1.3.70
This commit is contained in:
@@ -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 (сейчас нельзя задать).
|
||||||
@@ -55,7 +55,7 @@ spec:
|
|||||||
serviceAccountName: fission-console
|
serviceAccountName: fission-console
|
||||||
containers:
|
containers:
|
||||||
- name: console
|
- name: console
|
||||||
image: naeel/fission-console:v1.3.69
|
image: naeel/fission-console:v1.3.70
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 8090
|
- containerPort: 8090
|
||||||
|
|||||||
@@ -51,11 +51,11 @@ const fissionSourceTypeAnnotation = "fission-console/source-type"
|
|||||||
const defaultFunctionInvokeTimeout = 60 * time.Second
|
const defaultFunctionInvokeTimeout = 60 * time.Second
|
||||||
|
|
||||||
// handleFunctionsRoot обрабатывает запросы к /console/api/functions без имени функции.
|
// handleFunctionsRoot обрабатывает запросы к /console/api/functions без имени функции.
|
||||||
// GET → список всех функций, POST → создать новую.
|
// GET → список всех функций со статусами, POST → создать новую.
|
||||||
func (s *Server) handleFunctionsRoot(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleFunctionsRoot(w http.ResponseWriter, r *http.Request) {
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
s.handleList(fission.FunctionGVR)(w, r)
|
s.handleListFunctionsWithStatus(w, r)
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
s.handleCreateFunction(w, r)
|
s.handleCreateFunction(w, r)
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -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=<name>)
|
||||||
|
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)
|
||||||
|
}
|
||||||
+15
-2
@@ -102,7 +102,7 @@
|
|||||||
<div class="nubes">NUBES</div>
|
<div class="nubes">NUBES</div>
|
||||||
<div class="product">FISSION CONSOLE</div>
|
<div class="product">FISSION CONSOLE</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="font-size:0.65rem; color:var(--text-secondary); margin-left:10px; align-self:center; opacity:0.7;">v1.3.69</div>
|
<div style="font-size:0.65rem; color:var(--text-secondary); margin-left:10px; align-self:center; opacity:0.7;">v1.3.70</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row" style="margin:0;">
|
<div class="row" style="margin:0;">
|
||||||
<button class="btn ghost" onclick="reloadAll()">Refresh</button>
|
<button class="btn ghost" onclick="reloadAll()">Refresh</button>
|
||||||
@@ -142,10 +142,23 @@
|
|||||||
<div style="font-weight:600;">Функции</div>
|
<div style="font-weight:600;">Функции</div>
|
||||||
<div class="hint">Actions: view, edit code, invoke, delete</div>
|
<div class="hint">Actions: view, edit code, invoke, delete</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Легенда статусов -->
|
||||||
|
<div style="margin-bottom:12px; padding:8px; background:var(--bg-alt); border-radius:4px; font-size:0.9em; color:var(--text-secondary);">
|
||||||
|
<strong>Статусы функций:</strong>
|
||||||
|
<span style="margin-left:16px;">
|
||||||
|
<span style="display:inline-block; width:10px; height:10px; border-radius:50%; background:#2a2; vertical-align:middle;"></span> Готова
|
||||||
|
<span style="margin-left:12px; display:inline-block; width:10px; height:10px; border-radius:50%; background:#22a; vertical-align:middle;"></span> Холодная
|
||||||
|
<span style="margin-left:12px; display:inline-block; width:10px; height:10px; border-radius:50%; background:#aa2; vertical-align:middle;"></span> Сборка
|
||||||
|
<span style="margin-left:12px; display:inline-block; width:10px; height:10px; border-radius:50%; background:#a22; vertical-align:middle;"></span> Ошибка
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Имя</th>
|
<th>Имя</th>
|
||||||
|
<th title="Статус функции (Готова / Холодная / Сборка / Ошибка)">Статус</th>
|
||||||
<th title="Тип источника функции">Тип</th>
|
<th title="Тип источника функции">Тип</th>
|
||||||
<th>Создана</th>
|
<th>Создана</th>
|
||||||
<th>Изменена</th>
|
<th>Изменена</th>
|
||||||
@@ -485,7 +498,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions" style="justify-content:space-between; align-items:center;">
|
<div class="actions" style="justify-content:space-between; align-items:center;">
|
||||||
<span style="font-size:0.75rem; color:var(--text-secondary);">v1.3.69</span>
|
<span style="font-size:0.75rem; color:var(--text-secondary);">v1.3.70</span>
|
||||||
<button class="btn ghost" onclick="closeHelp()">Закрыть</button>
|
<button class="btn ghost" onclick="closeHelp()">Закрыть</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+16
-1
@@ -48,6 +48,20 @@ async function reloadAll() {
|
|||||||
const createdAt = ann['fission-console/created-at'] || meta.creationTimestamp || '-';
|
const createdAt = ann['fission-console/created-at'] || meta.creationTimestamp || '-';
|
||||||
const updatedAt = ann['fission-console/updated-at'] || createdAt;
|
const updatedAt = ann['fission-console/updated-at'] || createdAt;
|
||||||
const sourceType = ann['fission-console/source-type'] || 'code';
|
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 = '<span style="display:inline-block; width:12px; height:12px; border-radius:50%; background:' + statusColor + '; cursor:default;" title="' + h(statusTitle) + '"></span>';
|
||||||
|
|
||||||
const sourceIcon = sourceType === 'archive'
|
const sourceIcon = sourceType === 'archive'
|
||||||
? '<span title="Из архива (.zip)" style="font-size:1.1em; cursor:default;">📦</span>'
|
? '<span title="Из архива (.zip)" style="font-size:1.1em; cursor:default;">📦</span>'
|
||||||
: '<span title="Из кода (редактор)" style="font-size:1.1em; cursor:default;">📝</span>';
|
: '<span title="Из кода (редактор)" style="font-size:1.1em; cursor:default;">📝</span>';
|
||||||
@@ -58,7 +72,7 @@ async function reloadAll() {
|
|||||||
var infoData = h(JSON.stringify({
|
var infoData = h(JSON.stringify({
|
||||||
name: name, env: env, pkg: pkg, entrypoint: entrypoint, timeout: timeout,
|
name: name, env: env, pkg: pkg, entrypoint: entrypoint, timeout: timeout,
|
||||||
route: route, methods: methods, sourceType: sourceType,
|
route: route, methods: methods, sourceType: sourceType,
|
||||||
createdAt: createdAt, updatedAt: updatedAt, cron: cron
|
createdAt: createdAt, updatedAt: updatedAt, cron: cron, status: status
|
||||||
}));
|
}));
|
||||||
var actions = tfBadge +
|
var actions = tfBadge +
|
||||||
'<button class="btn ghost" onclick="openInfo(\'' + h(name) + '\', this)" data-info="' + infoData + '">Info</button> ' +
|
'<button class="btn ghost" onclick="openInfo(\'' + h(name) + '\', this)" data-info="' + infoData + '">Info</button> ' +
|
||||||
@@ -68,6 +82,7 @@ async function reloadAll() {
|
|||||||
'<button class="btn danger" onclick="removeFn(\'' + h(name) + '\')">Удалить</button>';
|
'<button class="btn danger" onclick="removeFn(\'' + h(name) + '\')">Удалить</button>';
|
||||||
return '<tr>' +
|
return '<tr>' +
|
||||||
'<td class="mono">' + h(name) + '</td>' +
|
'<td class="mono">' + h(name) + '</td>' +
|
||||||
|
'<td style="text-align:center;">' + statusDot + '</td>' +
|
||||||
'<td style="text-align:center;">' + sourceIcon + '</td>' +
|
'<td style="text-align:center;">' + sourceIcon + '</td>' +
|
||||||
'<td>' + timestampCell(createdAt) + '</td>' +
|
'<td>' + timestampCell(createdAt) + '</td>' +
|
||||||
'<td>' + timestampCell(updatedAt) + '</td>' +
|
'<td>' + timestampCell(updatedAt) + '</td>' +
|
||||||
|
|||||||
Reference in New Issue
Block a user