Files
sless/internal/api/handler/invoke.go
T
“Naeel” 88a0fb9db6 feat: notes-python CRUD example + runtime path/query forwarding
- invoke.go: forward sub-path and query string to function pods
- server.js v0.1.2: add _path, _query, _method to event
- server.py v0.1.1: add _path, _query, _method to event
- upload.go: bump runtime versions (nodejs20:v0.1.2, python3.11:v0.1.1)
- examples/notes-python: CRUD notes via sub-path routing
  - sql-runner: generic SQL executor for DDL jobs
  - notes: CRUD router (/add, /update, /delete)
  - notes-list: SELECT all notes
  - init.tf: create TABLE + INDEX on apply
2026-03-09 09:51:56 +04:00

77 lines
3.0 KiB
Go

// Изменено: 2026-03-09
// invoke.go — прокси-обработчик для вызова HTTP-триггеров функций.
// Маршрут: ANY /fn/{namespace}/{name} и /fn/{namespace}/{name}/**
// Не защищён auth-токеном — это публичный эндпоинт для вызова функций.
// Проксирует запрос к ClusterIP Service функции внутри кластера:
// http://{name}.sless-fn-{namespace}.svc.cluster.local:8080
// Sub-path и query string пробрасываются как есть:
// /fn/ns/notes/add?title=x → http://notes.sless-fn-ns.svc.../add?title=x
package handler
import (
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/gorilla/mux"
)
// httpClient используется для обращения к функциям внутри кластера.
// Таймаут 30s — достаточно для холодного старта функции.
var httpClient = &http.Client{Timeout: 30 * time.Second}
// InvokeFunction проксирует входящий запрос к Service функции в кластере.
// Namespace выбирается из пути, имя функции — тоже из пути.
// Сохраняет метод, тело, Content-Type, sub-path и query string.
func (h *Handler) InvokeFunction(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
ns := vars["namespace"]
name := vars["name"]
// Вычисляем sub-path после /fn/{namespace}/{name}
// Например: /fn/default/notes/add → subPath = /add
prefix := "/fn/" + ns + "/" + name
subPath := strings.TrimPrefix(r.URL.Path, prefix)
// Внутренний URL к Service функции (DNS внутри кластера)
target := fmt.Sprintf("http://%s.sless-fn-%s.svc.cluster.local:8080%s", name, ns, subPath)
// Пробрасываем query string если есть
if r.URL.RawQuery != "" {
target += "?" + r.URL.RawQuery
}
// Создаём проксируемый запрос с тем же методом и телом
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)
}