From 9b0089e15a03aa19fd77960a90ba175496547ce8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Thu, 7 May 2026 20:51:02 +0400 Subject: [PATCH] feat: logs modal + ESC close + /functions/:name/logs endpoint (v1.3.61) --- console/cmd/server/main.go | 7 ++++ console/deploy/console.yaml | 2 +- console/go.mod | 5 ++- console/internal/api/handlers.go | 61 ++++++++++++++++++++++++++++++++ console/internal/api/server.go | 4 +++ console/ui/index.html | 16 +++++++-- console/ui/js/app.js | 1 + console/ui/js/modals.js | 45 ++++++++++++++++++++++- 8 files changed, 136 insertions(+), 5 deletions(-) diff --git a/console/cmd/server/main.go b/console/cmd/server/main.go index fd22dec..7afae4a 100644 --- a/console/cmd/server/main.go +++ b/console/cmd/server/main.go @@ -14,6 +14,7 @@ import ( "fission-console/internal/auth" "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" ) @@ -37,6 +38,11 @@ func main() { log.Fatalf("create dynamic client: %v", err) } + kube, err := kubernetes.NewForConfig(cfg) + if err != nil { + log.Fatalf("create kubernetes client: %v", err) + } + authenticator := &auth.MultiAuthenticator{ JWT: auth.NewDeckAuthenticator(auth.DefaultDeckAPIs, nil), Demo: &auth.DemoAuthenticator{}, @@ -44,6 +50,7 @@ func main() { srv := api.NewServer(api.Config{ Dyn: dyn, + Kube: kube, Namespace: namespace, RouterURL: routerURL, HTTPTimeout: httpTimeout, diff --git a/console/deploy/console.yaml b/console/deploy/console.yaml index 5cedb59..d233fa6 100644 --- a/console/deploy/console.yaml +++ b/console/deploy/console.yaml @@ -52,7 +52,7 @@ spec: serviceAccountName: fission-console containers: - name: console - image: naeel/fission-console:v1.3.60 + image: naeel/fission-console:v1.3.61 imagePullPolicy: Always ports: - containerPort: 8090 diff --git a/console/go.mod b/console/go.mod index e531983..842e855 100644 --- a/console/go.mod +++ b/console/go.mod @@ -3,12 +3,15 @@ module fission-console go 1.25.0 require ( + golang.org/x/net v0.47.0 + k8s.io/api v0.34.1 k8s.io/apimachinery v0.34.1 k8s.io/client-go v0.34.1 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect @@ -16,6 +19,7 @@ require ( github.com/go-openapi/swag v0.23.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.7 // indirect @@ -27,7 +31,6 @@ require ( github.com/x448/float16 v0.8.4 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/net v0.47.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sys v0.38.0 // indirect golang.org/x/term v0.37.0 // indirect diff --git a/console/internal/api/handlers.go b/console/internal/api/handlers.go index 18b97ec..2c38d0b 100644 --- a/console/internal/api/handlers.go +++ b/console/internal/api/handlers.go @@ -25,6 +25,7 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + corev1 "k8s.io/api/core/v1" ) // validFuncName — RFC 1123 subdomain label: строчные буквы+цифры+дефис, без дефиса в начале/конце. @@ -332,6 +333,12 @@ func (s *Server) handleFunctionsAction(w http.ResponseWriter, r *http.Request) { return } + // /functions/:name/logs — логи подов функции + if len(parts) == 2 && parts[1] == "logs" && r.Method == http.MethodGet { + s.handleGetFunctionLogs(w, r, name) + return + } + http.NotFound(w, r) } @@ -1613,3 +1620,57 @@ func (s *Server) handleUpdateFunctionArchive(w http.ResponseWriter, r *http.Requ "source_type": "archive", }) } + +// handleGetFunctionLogs возвращает логи пода функции (последние 100 строк). +// Ищет под по лейблу functionName= в namespace пользователя. +func (s *Server) handleGetFunctionLogs(w http.ResponseWriter, r *http.Request, name string) { + ns := s.userNS(r) + ctx := r.Context() + + if s.kube == nil { + writeJSONError(w, http.StatusServiceUnavailable, "kubernetes client not available") + return + } + + labelSelector := "functionName=" + name + pods, err := s.kube.CoreV1().Pods(ns).List(ctx, metav1.ListOptions{ + LabelSelector: labelSelector, + }) + if err != nil { + writeJSONError(w, http.StatusInternalServerError, "list pods: "+err.Error()) + return + } + + if len(pods.Items) == 0 { + writeAnyJSON(w, http.StatusOK, map[string]any{ + "logs": "(нет запущенных подов для функции " + name + ")", + }) + return + } + + var allLogs strings.Builder + tailLines := int64(100) + for _, pod := range pods.Items { + req := s.kube.CoreV1().Pods(ns).GetLogs(pod.Name, &corev1.PodLogOptions{ + Container: "user-container", + TailLines: &tailLines, + }) + rc, err := req.Stream(ctx) + if err != nil { + allLogs.WriteString("[" + pod.Name + ": ошибка чтения логов: " + err.Error() + "]\n") + continue + } + data, _ := io.ReadAll(rc) + rc.Close() + if allLogs.Len() > 0 { + allLogs.WriteString("\n--- " + pod.Name + " ---\n") + } else { + allLogs.WriteString("--- " + pod.Name + " ---\n") + } + allLogs.Write(data) + } + + writeAnyJSON(w, http.StatusOK, map[string]any{ + "logs": allLogs.String(), + }) +} diff --git a/console/internal/api/server.go b/console/internal/api/server.go index 65662f1..b9b0834 100644 --- a/console/internal/api/server.go +++ b/console/internal/api/server.go @@ -21,6 +21,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" ) // defaultSATokenPath — путь к service account токену внутри pod-а. @@ -31,6 +32,7 @@ const defaultSATokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token" // Содержит все зависимости: kubernetes client, конфиги, кэши. type Server struct { dyn dynamic.Interface + kube kubernetes.Interface // typed client — только для логов подов ns string // системный namespace (fallback, обычно "fission") routerURL string http *http.Client @@ -62,6 +64,7 @@ type Server struct { // Config содержит все параметры для создания Server. type Config struct { Dyn dynamic.Interface + Kube kubernetes.Interface Namespace string RouterURL string HTTPTimeout time.Duration @@ -80,6 +83,7 @@ func NewServer(cfg Config) *Server { log.Printf("NewServer: storagesvcURL=%q", cfg.StoragesvcURL) return &Server{ dyn: cfg.Dyn, + kube: cfg.Kube, ns: cfg.Namespace, routerURL: cfg.RouterURL, http: &http.Client{Timeout: cfg.HTTPTimeout}, diff --git a/console/ui/index.html b/console/ui/index.html index 66f3ca8..b068ed0 100644 --- a/console/ui/index.html +++ b/console/ui/index.html @@ -102,7 +102,7 @@
NUBES
FISSION CONSOLE
-
v1.3.60
+
v1.3.61
@@ -384,6 +384,18 @@
+ + diff --git a/console/ui/js/app.js b/console/ui/js/app.js index 385c226..469d86e 100644 --- a/console/ui/js/app.js +++ b/console/ui/js/app.js @@ -51,6 +51,7 @@ async function reloadAll() { var actions = tfBadge + ' ' + ' ' + + ' ' + ''; return '' + '' + h(name) + '' + diff --git a/console/ui/js/modals.js b/console/ui/js/modals.js index cdeaebb..0bbdedb 100644 --- a/console/ui/js/modals.js +++ b/console/ui/js/modals.js @@ -46,7 +46,20 @@ function makeDraggable(modalId) { // Инициализация после загрузки DOM document.addEventListener('DOMContentLoaded', function() { - ['create-code-modal', 'create-archive-modal', 'edit-modal', 'help-modal', 'invoke-modal'].forEach(makeDraggable); + ['create-code-modal', 'create-archive-modal', 'edit-modal', 'help-modal', 'invoke-modal', 'logs-modal'].forEach(makeDraggable); + + // ESC закрывает активное модальное окно + document.addEventListener('keydown', function(e) { + if (e.key !== 'Escape') return; + var modals = ['create-code-modal', 'create-archive-modal', 'edit-modal', 'help-modal', 'invoke-modal', 'logs-modal']; + for (var i = 0; i < modals.length; i++) { + var el = document.getElementById(modals[i]); + if (el && el.classList.contains('open')) { + el.classList.remove('open'); + break; + } + } + }); }); // showModalError — показывает ошибку внутри модалки. @@ -136,3 +149,33 @@ async function submitInvoke() { btn.disabled = false; } } + +// openLogs — открывает модалку с логами функции. +async function openLogs(name) { + S.currentLogs = name; + document.getElementById('logs-title').textContent = 'Логи: ' + name; + document.getElementById('logs-output').value = 'Загрузка...'; + document.getElementById('logs-modal').classList.add('open'); + await _fetchLogs(name); +} + +function closeLogs() { + document.getElementById('logs-modal').classList.remove('open'); + S.currentLogs = null; +} + +async function refreshLogs() { + if (S.currentLogs) await _fetchLogs(S.currentLogs); +} + +async function _fetchLogs(name) { + var out = document.getElementById('logs-output'); + try { + var data = await getJSON(API_BASE + '/functions/' + encodeURIComponent(name) + '/logs'); + out.value = data.logs || '(нет логов)'; + // прокручиваем вниз + out.scrollTop = out.scrollHeight; + } catch (e) { + out.value = 'Ошибка загрузки логов: ' + e.message; + } +}