feat(funcs-console): показывать sless_service вместе с функциями — единый листинг
This commit is contained in:
@@ -176,6 +176,18 @@
|
|||||||
color: #f85149;
|
color: #f85149;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badge-kind-service {
|
||||||
|
background: #0d2538;
|
||||||
|
border: 1px solid #1158a7;
|
||||||
|
color: #58a6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-kind-function {
|
||||||
|
background: #1a2538;
|
||||||
|
border: 1px solid #30363d;
|
||||||
|
color: #8b949e;
|
||||||
|
}
|
||||||
|
|
||||||
.fn-url {
|
.fn-url {
|
||||||
font-size: 0.76rem;
|
font-size: 0.76rem;
|
||||||
color: #58a6ff;
|
color: #58a6ff;
|
||||||
@@ -328,7 +340,10 @@
|
|||||||
var byFn = data.triggersByFn || {};
|
var byFn = data.triggersByFn || {};
|
||||||
|
|
||||||
document.getElementById('ns-label').textContent = ns;
|
document.getElementById('ns-label').textContent = ns;
|
||||||
document.getElementById('summary').textContent = 'Функций: ' + fns.length;
|
var svcsCount = fns.filter(function(f){ return f.kind === 'service'; }).length;
|
||||||
|
var fnsCount = fns.filter(function(f){ return f.kind !== 'service'; }).length;
|
||||||
|
document.getElementById('summary').textContent =
|
||||||
|
'Функций: ' + fnsCount + ' | Сервисов: ' + svcsCount + ' | Всего: ' + fns.length;
|
||||||
|
|
||||||
var list = document.getElementById('fn-list');
|
var list = document.getElementById('fn-list');
|
||||||
if (fns.length === 0) {
|
if (fns.length === 0) {
|
||||||
@@ -348,11 +363,29 @@
|
|||||||
|
|
||||||
$txt(hdr, 'span', 'fn-name', fn.name);
|
$txt(hdr, 'span', 'fn-name', fn.name);
|
||||||
$txt(hdr, 'span', 'badge badge-runtime', fn.runtime);
|
$txt(hdr, 'span', 'badge badge-runtime', fn.runtime);
|
||||||
$txt(hdr, 'span', 'badge badge-' + fn.phase, fn.phase);
|
$txt(hdr, 'span', 'badge badge-' + fn.phase, fn.phase || 'Pending');
|
||||||
|
if (fn.kind) {
|
||||||
|
var kindLabel = fn.kind === 'service' ? 'always-on' : 'job';
|
||||||
|
$txt(hdr, 'span', 'badge badge-kind-' + fn.kind, kindLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// sless_service имеет прямой URL из статуса деплоя
|
||||||
|
if (fn.url) {
|
||||||
|
var a = document.createElement('a');
|
||||||
|
a.className = 'fn-url';
|
||||||
|
a.href = fn.url;
|
||||||
|
a.target = '_blank';
|
||||||
|
a.rel = 'noopener noreferrer';
|
||||||
|
a.textContent = fn.url;
|
||||||
|
a.addEventListener('click', function (e) { e.stopPropagation(); });
|
||||||
|
hdr.appendChild(a);
|
||||||
|
}
|
||||||
|
|
||||||
for (var t = 0; t < triggers.length; t++) {
|
for (var t = 0; t < triggers.length; t++) {
|
||||||
var tr = triggers[t];
|
var tr = triggers[t];
|
||||||
if (tr.type === 'http') {
|
if (tr.type === 'http') {
|
||||||
|
// Для job-style функций URL берём из триггера; для сервисов уже показан fn.url
|
||||||
|
if (!fn.url) {
|
||||||
var url = extURL ? extURL + '/fn/' + ns + '/' + fn.name : (tr.url || '');
|
var url = extURL ? extURL + '/fn/' + ns + '/' + fn.name : (tr.url || '');
|
||||||
if (url) {
|
if (url) {
|
||||||
var a = document.createElement('a');
|
var a = document.createElement('a');
|
||||||
@@ -364,6 +397,7 @@
|
|||||||
a.addEventListener('click', function (e) { e.stopPropagation(); });
|
a.addEventListener('click', function (e) { e.stopPropagation(); });
|
||||||
hdr.appendChild(a);
|
hdr.appendChild(a);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
hdr.appendChild(makeTriggerBtn(tr, ns));
|
hdr.appendChild(makeTriggerBtn(tr, ns));
|
||||||
} else if (tr.type === 'cron') {
|
} else if (tr.type === 'cron') {
|
||||||
$txt(hdr, 'span', 'fn-cron', 'cron: ' + tr.schedule);
|
$txt(hdr, 'span', 'fn-cron', 'cron: ' + tr.schedule);
|
||||||
|
|||||||
+51
-4
@@ -1,4 +1,4 @@
|
|||||||
// Изменено: 2026-04-25 (добавлена HTML web-консоль, proxy endpoints для source/triggers)
|
// Изменено: 2026-03-21 (добавлен вывод sless_service вместе с функциями — пользователь видит оба типа как «функции»)
|
||||||
// main.go — глобальный HTTP сервис листинга функций пользователя.
|
// main.go — глобальный HTTP сервис листинга функций пользователя.
|
||||||
// Развёрнут ОДИН РАЗ в namespace sless; работает для ВСЕХ пользователей.
|
// Развёрнут ОДИН РАЗ в namespace sless; работает для ВСЕХ пользователей.
|
||||||
// Не связан с terraform — деплоится манифестом deployments/k8s/funcs-service.yaml.
|
// Не связан с terraform — деплоится манифестом deployments/k8s/funcs-service.yaml.
|
||||||
@@ -39,7 +39,9 @@ import (
|
|||||||
//go:embed index.html
|
//go:embed index.html
|
||||||
var htmlPageTemplate string
|
var htmlPageTemplate string
|
||||||
|
|
||||||
// fnResponse — ответ /v1/namespaces/{ns}/functions (подмножество полей оператора)
|
// fnResponse — унифицированная запись функции для листинга.
|
||||||
|
// Kind="function" — job-style (запускается per-request).
|
||||||
|
// Kind="service" — always-on Deployment (всегда готов к запросам, имеет прямой URL).
|
||||||
type fnResponse struct {
|
type fnResponse struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Runtime string `json:"runtime"`
|
Runtime string `json:"runtime"`
|
||||||
@@ -47,6 +49,19 @@ type fnResponse struct {
|
|||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
LastBuiltAt string `json:"last_built_at"`
|
LastBuiltAt string `json:"last_built_at"`
|
||||||
|
Kind string `json:"kind"` // "function" | "service"
|
||||||
|
URL string `json:"url,omitempty"` // прямой URL для sless_service
|
||||||
|
}
|
||||||
|
|
||||||
|
// svcResponse — ответ /v1/namespaces/{ns}/services (подмножество полей оператора).
|
||||||
|
type svcResponse struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Runtime string `json:"runtime"`
|
||||||
|
Phase string `json:"phase"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
LastBuiltAt string `json:"last_built_at"`
|
||||||
|
URL string `json:"url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// trResponse — ответ /v1/namespaces/{ns}/triggers
|
// trResponse — ответ /v1/namespaces/{ns}/triggers
|
||||||
@@ -182,13 +197,35 @@ func handleFuncsNS(operatorURL, externalURL, serviceToken string, exclude map[st
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetchAndRender загружает данные из оператора и отдаёт ответ: HTML (браузер) или plain text (curl).
|
// fetchAndRender загружает functions + services из оператора и отдаёт ответ.
|
||||||
|
// Оба типа объединяются в единый список — пользователь видит их как «функции».
|
||||||
func fetchAndRender(w http.ResponseWriter, r *http.Request, operatorURL, externalURL, authHeader, ns string, exclude map[string]bool) {
|
func fetchAndRender(w http.ResponseWriter, r *http.Request, operatorURL, externalURL, authHeader, ns string, exclude map[string]bool) {
|
||||||
fns, err := apiGet[[]fnResponse](operatorURL+"/v1/namespaces/"+ns+"/functions", authHeader)
|
fns, err := apiGet[[]fnResponse](operatorURL+"/v1/namespaces/"+ns+"/functions", authHeader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, fmt.Sprintf("operator error (functions): %s\n", err), http.StatusBadGateway)
|
http.Error(w, fmt.Sprintf("operator error (functions): %s\n", err), http.StatusBadGateway)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
for i := range fns {
|
||||||
|
fns[i].Kind = "function"
|
||||||
|
}
|
||||||
|
|
||||||
|
// sless_service — always-on Deployment + URL. Объединяем с функциями.
|
||||||
|
svcs, svcErr := apiGet[[]svcResponse](operatorURL+"/v1/namespaces/"+ns+"/services", authHeader)
|
||||||
|
if svcErr == nil {
|
||||||
|
for _, svc := range svcs {
|
||||||
|
fns = append(fns, fnResponse{
|
||||||
|
Name: svc.Name,
|
||||||
|
Runtime: svc.Runtime,
|
||||||
|
Phase: svc.Phase,
|
||||||
|
Message: svc.Message,
|
||||||
|
CreatedAt: svc.CreatedAt,
|
||||||
|
LastBuiltAt: svc.LastBuiltAt,
|
||||||
|
Kind: "service",
|
||||||
|
URL: svc.URL,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
trs, err := apiGet[[]trResponse](operatorURL+"/v1/namespaces/"+ns+"/triggers", authHeader)
|
trs, err := apiGet[[]trResponse](operatorURL+"/v1/namespaces/"+ns+"/triggers", authHeader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, fmt.Sprintf("operator error (triggers): %s\n", err), http.StatusBadGateway)
|
http.Error(w, fmt.Sprintf("operator error (triggers): %s\n", err), http.StatusBadGateway)
|
||||||
@@ -279,7 +316,10 @@ func servePlainText(w http.ResponseWriter, ns, externalURL string, fns []fnRespo
|
|||||||
} else {
|
} else {
|
||||||
sb.WriteString(" active: нет\n")
|
sb.WriteString(" active: нет\n")
|
||||||
}
|
}
|
||||||
if len(it.httpT) > 0 {
|
// Для sless_service — прямой URL из статуса деплоя
|
||||||
|
if fn.URL != "" {
|
||||||
|
sb.WriteString(fmt.Sprintf(" url: %s\n", fn.URL))
|
||||||
|
} else if len(it.httpT) > 0 {
|
||||||
url := it.httpT[0].URL
|
url := it.httpT[0].URL
|
||||||
if externalURL != "" {
|
if externalURL != "" {
|
||||||
url = externalURL + "/fn/" + ns + "/" + fn.Name
|
url = externalURL + "/fn/" + ns + "/" + fn.Name
|
||||||
@@ -415,6 +455,13 @@ func isValidK8sName(s string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func buildComment(fn fnResponse, httpT, cronT []trResponse) string {
|
func buildComment(fn fnResponse, httpT, cronT []trResponse) string {
|
||||||
|
if fn.Kind == "service" {
|
||||||
|
active := "неактивна"
|
||||||
|
if fn.Phase == "Ready" {
|
||||||
|
active = "активна"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("always-on сервис (%s) — %s, %s", fn.Runtime, fn.Phase, active)
|
||||||
|
}
|
||||||
if len(httpT) > 0 {
|
if len(httpT) > 0 {
|
||||||
active := "активна"
|
active := "активна"
|
||||||
if !httpT[0].Active {
|
if !httpT[0].Active {
|
||||||
|
|||||||
Reference in New Issue
Block a user