feat: logs modal + ESC close + /functions/:name/logs endpoint (v1.3.61)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
+4
-1
@@ -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
|
||||
|
||||
@@ -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=<name> в 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(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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},
|
||||
|
||||
+14
-2
@@ -102,7 +102,7 @@
|
||||
<div class="nubes">NUBES</div>
|
||||
<div class="product">FISSION CONSOLE</div>
|
||||
</div>
|
||||
<div style="font-size:0.65rem; color:var(--text-secondary); margin-left:10px; align-self:center; opacity:0.7;">v1.3.60</div>
|
||||
<div style="font-size:0.65rem; color:var(--text-secondary); margin-left:10px; align-self:center; opacity:0.7;">v1.3.61</div>
|
||||
</div>
|
||||
<div class="row" style="margin:0;">
|
||||
<button class="btn ghost" onclick="reloadAll()">Refresh</button>
|
||||
@@ -384,6 +384,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="logs-modal" class="modal">
|
||||
<div class="panel" style="width:700px; max-width:95vw;">
|
||||
<h3 id="logs-title">Логи функции</h3>
|
||||
<textarea id="logs-output" readonly
|
||||
style="width:100%; height:340px; font-family:monospace; font-size:12px; background:var(--bg-alt); color:var(--text-primary); border:1px solid var(--border); border-radius:6px; padding:10px; resize:vertical; white-space:pre;"></textarea>
|
||||
<div class="actions">
|
||||
<button class="btn ghost" onclick="closeLogs()">Закрыть</button>
|
||||
<button class="btn ghost" onclick="refreshLogs()">Обновить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="help-modal" class="modal">
|
||||
<div class="panel">
|
||||
<h3>Help</h3>
|
||||
@@ -462,7 +474,7 @@
|
||||
</div>
|
||||
|
||||
<div class="actions" style="justify-content:space-between; align-items:center;">
|
||||
<span style="font-size:0.75rem; color:var(--text-secondary);">v1.3.60</span>
|
||||
<span style="font-size:0.75rem; color:var(--text-secondary);">v1.3.61</span>
|
||||
<button class="btn ghost" onclick="closeHelp()">Закрыть</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -51,6 +51,7 @@ async function reloadAll() {
|
||||
var actions = tfBadge +
|
||||
'<button class="btn ghost" onclick="openEdit(\'' + h(name) + '\')">Ред.</button> ' +
|
||||
'<button class="btn ghost" onclick="openInvoke(\'' + h(name) + '\')">Вызов</button> ' +
|
||||
'<button class="btn ghost" onclick="openLogs(\'' + h(name) + '\')">Логи</button> ' +
|
||||
'<button class="btn danger" onclick="removeFn(\'' + h(name) + '\')">Удалить</button>';
|
||||
return '<tr>' +
|
||||
'<td class="mono">' + h(name) + '</td>' +
|
||||
|
||||
+44
-1
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user