feat: funcs-service v0.1.3 — /funcs/<namespace> URL without token

- /funcs/<namespace>: user opens URL in browser, no auth needed
  service uses SLESS_SERVICE_TOKEN to query operator internally
- /funcs?token=<jwt>: 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)
This commit is contained in:
Naeel
2026-03-18 16:11:17 +03:00
parent e8cd62e171
commit 38bb494ed5
2 changed files with 46 additions and 19 deletions
+3 -1
View File
@@ -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:
+43 -18
View File
@@ -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/<namespace> (без токена пользователя)
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> — без токена, 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 <token> required\n", http.StatusUnauthorized)
return
// Вариант 1: /funcs/<namespace> — 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/<namespace>\n", http.StatusBadRequest)
return
}
// Используем serviceToken сервиса, если задан
if serviceToken != "" {
authHeader = "Bearer " + serviceToken
}
// authHeader может остаться пустым — оператор ответит 401 если не настроен
} else {
// Вариант 2: ?token=<jwt> — токен как query param (можно bookmarkнуть URL)
token := r.URL.Query().Get("token")
// Вариант 3: Authorization: Bearer <jwt> — стандартный заголовок
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/<namespace>\n", externalURL)
fmt.Fprintf(w, " %s/funcs?token=<jwt>\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)