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:
@@ -27,7 +27,7 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: funcs
|
- name: funcs
|
||||||
image: naeel/sless-funcs-service:v0.1.1
|
image: naeel/sless-funcs-service:v0.1.3
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 8090
|
- containerPort: 8090
|
||||||
env:
|
env:
|
||||||
@@ -38,6 +38,8 @@ spec:
|
|||||||
# Системные функции, скрытые из листинга
|
# Системные функции, скрытые из листинга
|
||||||
- name: SLESS_EXCLUDE
|
- name: SLESS_EXCLUDE
|
||||||
value: "event-writer,event-monitor,event-cleaner"
|
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
|
- name: PORT
|
||||||
value: "8090"
|
value: "8090"
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
|
|||||||
+43
-18
@@ -56,6 +56,8 @@ func main() {
|
|||||||
operatorURL := strings.TrimRight(env("SLESS_OPERATOR_URL", "http://sless-operator.sless.svc.cluster.local:9090"), "/")
|
operatorURL := strings.TrimRight(env("SLESS_OPERATOR_URL", "http://sless-operator.sless.svc.cluster.local:9090"), "/")
|
||||||
externalURL := strings.TrimRight(env("SLESS_EXTERNAL_URL", ""), "/")
|
externalURL := strings.TrimRight(env("SLESS_EXTERNAL_URL", ""), "/")
|
||||||
port := env("PORT", "8090")
|
port := env("PORT", "8090")
|
||||||
|
// serviceToken — токен сервиса для запросов к оператору по /funcs/<namespace> (без токена пользователя)
|
||||||
|
serviceToken := os.Getenv("SLESS_SERVICE_TOKEN")
|
||||||
|
|
||||||
exclude := map[string]bool{}
|
exclude := map[string]bool{}
|
||||||
for _, n := range strings.Split(os.Getenv("SLESS_EXCLUDE"), ",") {
|
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 токена
|
// /health — для liveness/readiness probe без Bearer токена
|
||||||
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
fmt.Fprintln(w, "ok")
|
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.Printf("sless-funcs-service listening on :%s (operator: %s)", port, operatorURL)
|
||||||
log.Fatal(http.ListenAndServe(":"+port, nil))
|
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) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.URL.Path != "/funcs" {
|
var namespace, authHeader string
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
|
// Вариант 1: /funcs/<namespace> — namespace прямо в URL, токен не нужен.
|
||||||
if token == "" {
|
// Namespace виден в URL каждой функции — не является секретом.
|
||||||
http.Error(w, "Authorization: Bearer <token> required\n", http.StatusUnauthorized)
|
if strings.HasPrefix(r.URL.Path, "/funcs/") {
|
||||||
return
|
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)
|
fns, err := apiGet[[]fnResponse](operatorURL+"/v1/namespaces/"+namespace+"/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)
|
||||||
|
|||||||
Reference in New Issue
Block a user