diff --git a/services/funcs/index.html b/services/funcs/index.html index 02584ba..3aaab6f 100644 --- a/services/funcs/index.html +++ b/services/funcs/index.html @@ -176,6 +176,18 @@ 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 { font-size: 0.76rem; color: #58a6ff; @@ -328,7 +340,10 @@ var byFn = data.triggersByFn || {}; 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'); if (fns.length === 0) { @@ -348,21 +363,40 @@ $txt(hdr, 'span', 'fn-name', fn.name); $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++) { var tr = triggers[t]; if (tr.type === 'http') { - var url = extURL ? extURL + '/fn/' + ns + '/' + fn.name : (tr.url || ''); - if (url) { - var a = document.createElement('a'); - a.className = 'fn-url'; - a.href = url; - a.target = '_blank'; - a.rel = 'noopener noreferrer'; - a.textContent = url; - a.addEventListener('click', function (e) { e.stopPropagation(); }); - hdr.appendChild(a); + // Для job-style функций URL берём из триггера; для сервисов уже показан fn.url + if (!fn.url) { + var url = extURL ? extURL + '/fn/' + ns + '/' + fn.name : (tr.url || ''); + if (url) { + var a = document.createElement('a'); + a.className = 'fn-url'; + a.href = url; + a.target = '_blank'; + a.rel = 'noopener noreferrer'; + a.textContent = url; + a.addEventListener('click', function (e) { e.stopPropagation(); }); + hdr.appendChild(a); + } } hdr.appendChild(makeTriggerBtn(tr, ns)); } else if (tr.type === 'cron') { diff --git a/services/funcs/main.go b/services/funcs/main.go index bc33ca3..1af680d 100644 --- a/services/funcs/main.go +++ b/services/funcs/main.go @@ -1,4 +1,4 @@ -// Изменено: 2026-04-25 (добавлена HTML web-консоль, proxy endpoints для source/triggers) +// Изменено: 2026-03-21 (добавлен вывод sless_service вместе с функциями — пользователь видит оба типа как «функции») // main.go — глобальный HTTP сервис листинга функций пользователя. // Развёрнут ОДИН РАЗ в namespace sless; работает для ВСЕХ пользователей. // Не связан с terraform — деплоится манифестом deployments/k8s/funcs-service.yaml. @@ -39,7 +39,9 @@ import ( //go:embed index.html var htmlPageTemplate string -// fnResponse — ответ /v1/namespaces/{ns}/functions (подмножество полей оператора) +// fnResponse — унифицированная запись функции для листинга. +// Kind="function" — job-style (запускается per-request). +// Kind="service" — always-on Deployment (всегда готов к запросам, имеет прямой URL). type fnResponse struct { Name string `json:"name"` Runtime string `json:"runtime"` @@ -47,6 +49,19 @@ type fnResponse struct { Message string `json:"message"` CreatedAt string `json:"created_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 @@ -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) { fns, err := apiGet[[]fnResponse](operatorURL+"/v1/namespaces/"+ns+"/functions", authHeader) if err != nil { http.Error(w, fmt.Sprintf("operator error (functions): %s\n", err), http.StatusBadGateway) 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) if err != nil { 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 { 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 if externalURL != "" { url = externalURL + "/fn/" + ns + "/" + fn.Name @@ -415,6 +455,13 @@ func isValidK8sName(s string) bool { } 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 { active := "активна" if !httpT[0].Active {