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
This commit is contained in:
“Naeel”
2026-03-09 09:51:56 +04:00
parent 0066d9c0f9
commit 88a0fb9db6
20 changed files with 344 additions and 11 deletions
+17 -6
View File
@@ -1,10 +1,11 @@
// Изменено: 2026-03-07
// Изменено: 2026-03-09
// invoke.go — прокси-обработчик для вызова HTTP-триггеров функций.
// Маршрут: ANY /fn/{namespace}/{name}
// Маршрут: ANY /fn/{namespace}/{name} и /fn/{namespace}/{name}/**
// Не защищён auth-токеном — это публичный эндпоинт для вызова функций.
// Проксирует запрос к ClusterIP Service функции внутри кластера:
// http://{name}.sless-fn-{namespace}.svc.cluster.local:8080
// Функция должна быть задеплоена (HTTP триггер активен).
// Sub-path и query string пробрасываются как есть:
// /fn/ns/notes/add?title=x → http://notes.sless-fn-ns.svc.../add?title=x
package handler
@@ -12,6 +13,7 @@ import (
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/gorilla/mux"
@@ -23,15 +25,24 @@ var httpClient = &http.Client{Timeout: 30 * time.Second}
// InvokeFunction проксирует входящий запрос к Service функции в кластере.
// Namespace выбирается из пути, имя функции — тоже из пути.
// Сохраняет метод, тело, и Content-Type заголовок оригинального запроса.
// Сохраняет метод, тело, 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 внутри кластера)
// Namespace функций: sless-fn-{user-namespace}
target := fmt.Sprintf("http://%s.sless-fn-%s.svc.cluster.local:8080", name, ns)
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)
+2 -2
View File
@@ -31,9 +31,9 @@ import (
func runtimeBaseImage(runtime string) (string, error) {
switch runtime {
case "python3.11":
return "naeel/sless-runtime-python3.11:v0.1.0", nil
return "naeel/sless-runtime-python3.11:v0.1.1", nil
case "nodejs20":
return "naeel/sless-runtime-nodejs20:v0.1.1", nil
return "naeel/sless-runtime-nodejs20:v0.1.2", nil
default:
return "", fmt.Errorf("unsupported runtime: %q (supported: python3.11, nodejs20)", runtime)
}