Проблема: wildcard DNS *.fn.kube5s.ru недоступен.
Решение: прокси через sless-api.kube5s.ru/fn/{ns}/{name}.
- handler/invoke.go: прокси к Service функции внутри кластера
- router.go: /fn/ без auth токена, /v1/ с auth (gorilla Use())
- config.go: поле ExternalURL (EXTERNAL_URL env)
- trigger_controller.go: если ExternalURL задан — URL = ExternalURL/fn/{ns}/{fn}
иначе fallback: Ingress + поддомен (прежнее поведение)
- operator.yaml: EXTERNAL_URL=https://sless-api.kube5s.ru, image v0.1.5
Оператор v0.1.5 задеплоен.
E2E: curl https://sless-api.kube5s.ru/fn/default/hello-node → {"message":"Hello, Naeel! (nodejs20)"}
66 lines
2.7 KiB
Go
66 lines
2.7 KiB
Go
// Изменено: 2026-03-07
|
|
// invoke.go — прокси-обработчик для вызова HTTP-триггеров функций.
|
|
// Маршрут: ANY /fn/{namespace}/{name}
|
|
// Не защищён auth-токеном — это публичный эндпоинт для вызова функций.
|
|
// Проксирует запрос к ClusterIP Service функции внутри кластера:
|
|
// http://{name}.sless-fn-{namespace}.svc.cluster.local:8080
|
|
// Функция должна быть задеплоена (HTTP триггер активен).
|
|
|
|
package handler
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/gorilla/mux"
|
|
)
|
|
|
|
// httpClient используется для обращения к функциям внутри кластера.
|
|
// Таймаут 30s — достаточно для холодного старта функции.
|
|
var httpClient = &http.Client{Timeout: 30 * time.Second}
|
|
|
|
// InvokeFunction проксирует входящий запрос к Service функции в кластере.
|
|
// Namespace выбирается из пути, имя функции — тоже из пути.
|
|
// Сохраняет метод, тело, и Content-Type заголовок оригинального запроса.
|
|
func (h *Handler) InvokeFunction(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
ns := vars["namespace"]
|
|
name := vars["name"]
|
|
|
|
// Внутренний URL к Service функции (DNS внутри кластера)
|
|
// Namespace функций: sless-fn-{user-namespace}
|
|
target := fmt.Sprintf("http://%s.sless-fn-%s.svc.cluster.local:8080", name, ns)
|
|
|
|
// Создаём проксируемый запрос с тем же методом и телом
|
|
proxyReq, err := http.NewRequestWithContext(r.Context(), r.Method, target, r.Body)
|
|
if err != nil {
|
|
h.Log.Error("invoke: failed to create proxy request", "err", err, "ns", ns, "fn", name)
|
|
writeJSON(w, http.StatusInternalServerError, errResp("failed to create proxy request"))
|
|
return
|
|
}
|
|
|
|
// Пробрасываем Content-Type если есть
|
|
if ct := r.Header.Get("Content-Type"); ct != "" {
|
|
proxyReq.Header.Set("Content-Type", ct)
|
|
}
|
|
|
|
resp, err := httpClient.Do(proxyReq)
|
|
if err != nil {
|
|
h.Log.Error("invoke: function unreachable", "err", err, "ns", ns, "fn", name, "target", target)
|
|
writeJSON(w, http.StatusBadGateway, errResp("function unreachable: "+err.Error()))
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Копируем заголовки и статус из ответа функции
|
|
for k, vals := range resp.Header {
|
|
for _, v := range vals {
|
|
w.Header().Add(k, v)
|
|
}
|
|
}
|
|
w.WriteHeader(resp.StatusCode)
|
|
_, _ = io.Copy(w, resp.Body)
|
|
}
|