From 38bb494ed544058728499d28dd6759807c5a17b6 Mon Sep 17 00:00:00 2001 From: Naeel Date: Wed, 18 Mar 2026 16:11:17 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20funcs-service=20v0.1.3=20=E2=80=94=20/f?= =?UTF-8?q?uncs/=20URL=20without=20token?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /funcs/: user opens URL in browser, no auth needed service uses SLESS_SERVICE_TOKEN to query operator internally - /funcs?token=: token as query param (bookmarkable URL) - /funcs: returns usage hint with both URL formats - SLESS_SERVICE_TOKEN set via kubectl set env (not stored in repo) --- deployments/k8s/funcs-service.yaml | 4 +- services/funcs/main.go | 61 +++++++++++++++++++++--------- 2 files changed, 46 insertions(+), 19 deletions(-) diff --git a/deployments/k8s/funcs-service.yaml b/deployments/k8s/funcs-service.yaml index 947fb4b..47a5d84 100644 --- a/deployments/k8s/funcs-service.yaml +++ b/deployments/k8s/funcs-service.yaml @@ -27,7 +27,7 @@ spec: spec: containers: - name: funcs - image: naeel/sless-funcs-service:v0.1.1 + image: naeel/sless-funcs-service:v0.1.3 ports: - containerPort: 8090 env: @@ -38,6 +38,8 @@ spec: # Системные функции, скрытые из листинга - name: SLESS_EXCLUDE value: "event-writer,event-monitor,event-cleaner" + # Токен сервиса задаётся через kubectl set env или Secret — не хранится в репо + # kubectl set env deployment/sless-funcs-service -n sless SLESS_SERVICE_TOKEN="$(cat secrets/test.token)" - name: PORT value: "8090" livenessProbe: diff --git a/services/funcs/main.go b/services/funcs/main.go index 9816fbb..9833267 100644 --- a/services/funcs/main.go +++ b/services/funcs/main.go @@ -56,6 +56,8 @@ func main() { operatorURL := strings.TrimRight(env("SLESS_OPERATOR_URL", "http://sless-operator.sless.svc.cluster.local:9090"), "/") externalURL := strings.TrimRight(env("SLESS_EXTERNAL_URL", ""), "/") port := env("PORT", "8090") + // serviceToken — токен сервиса для запросов к оператору по /funcs/ (без токена пользователя) + serviceToken := os.Getenv("SLESS_SERVICE_TOKEN") exclude := map[string]bool{} for _, n := range strings.Split(os.Getenv("SLESS_EXCLUDE"), ",") { @@ -64,38 +66,61 @@ func main() { } } - http.HandleFunc("/funcs", handler(operatorURL, externalURL, exclude)) + http.HandleFunc("/funcs", handler(operatorURL, externalURL, serviceToken, exclude)) // /health — для liveness/readiness probe без Bearer токена http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") fmt.Fprintln(w, "ok") }) + // /funcs/ — без токена, namespace виден в URL всех функций + http.HandleFunc("/funcs/", handler(operatorURL, externalURL, serviceToken, exclude)) log.Printf("sless-funcs-service listening on :%s (operator: %s)", port, operatorURL) log.Fatal(http.ListenAndServe(":"+port, nil)) } -func handler(operatorURL, externalURL string, exclude map[string]bool) http.HandlerFunc { +func handler(operatorURL, externalURL, serviceToken string, exclude map[string]bool) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/funcs" { - http.NotFound(w, r) - return - } + var namespace, authHeader string - token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") - if token == "" { - http.Error(w, "Authorization: Bearer required\n", http.StatusUnauthorized) - return + // Вариант 1: /funcs/ — namespace прямо в URL, токен не нужен. + // Namespace виден в URL каждой функции — не является секретом. + if strings.HasPrefix(r.URL.Path, "/funcs/") { + namespace = strings.TrimPrefix(r.URL.Path, "/funcs/") + namespace = strings.Trim(namespace, "/") + if namespace == "" { + http.Error(w, "usage: /funcs/\n", http.StatusBadRequest) + return + } + // Используем serviceToken сервиса, если задан + if serviceToken != "" { + authHeader = "Bearer " + serviceToken + } + // authHeader может остаться пустым — оператор ответит 401 если не настроен + } else { + // Вариант 2: ?token= — токен как query param (можно bookmarkнуть URL) + token := r.URL.Query().Get("token") + // Вариант 3: Authorization: Bearer — стандартный заголовок + if token == "" { + token = strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") + } + if token == "" { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprintf(w, "Укажите namespace в URL или передайте токен:\n\n") + fmt.Fprintf(w, " %s/funcs/\n", externalURL) + fmt.Fprintf(w, " %s/funcs?token=\n", externalURL) + return + } + sub, err := subFromJWT(token) + if err != nil { + http.Error(w, fmt.Sprintf("invalid token: %s\n", err), http.StatusUnauthorized) + return + } + namespace = namespaceFromSub(sub) + authHeader = "Bearer " + token } - sub, err := subFromJWT(token) - if err != nil { - http.Error(w, fmt.Sprintf("invalid token: %s\n", err), http.StatusUnauthorized) - return - } - namespace := namespaceFromSub(sub) - authHeader := "Bearer " + token - fns, err := apiGet[[]fnResponse](operatorURL+"/v1/namespaces/"+namespace+"/functions", authHeader) if err != nil { http.Error(w, fmt.Sprintf("operator error (functions): %s\n", err), http.StatusBadGateway)