From d7fda15d35218b935d6c922fbf14b61f16cfdf28 Mon Sep 17 00:00:00 2001 From: Naeel Date: Thu, 19 Mar 2026 21:33:41 +0300 Subject: [PATCH] feat: Go runtime v0.1.1 (pgx/v5), stress-go-pgstorm, fix invoke.go dynamic timeout, nginx ingress timeout 900s MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - runtimes/go1.23: добавлен pgx/v5 v5.7.2 в go.mod, сгенерирован go.sum - runtimes/go1.23/Dockerfile: один stage golang:1.23-alpine, go mod download кеширует зависимости - internal/builder/context.go: тег Go runtime v0.1.0 → v0.1.1 - internal/api/handler/invoke.go: таймаут прокси-клиента теперь динамический из Function.Spec.TimeoutSec + 5s буфер (был хардкод 30s) - examples/POSTGRES/code/stress-go-pgstorm/handler.go: новая функция, 100 горутин, pgxpool, INSERT/COUNT/MAX, параметры: workers/duration_sec/max_delay_ms - examples/POSTGRES/resources.tf: добавлены sless_function.stress_go_pgstorm + trigger, timeout_sec=700 - deployments/k8s/operator.yaml: nginx ingress proxy-read-timeout=900s, proxy-send-timeout=900s - examples/*/main.tf: исправлен URL deck-api-test.ngcloud.ru → deck-test.ngcloud.ru (все 9 файлов) - Оператор: v0.1.39 (pgx/v5) → v0.1.40 (dynamic timeout) --- deployments/k8s/operator.yaml | 2 + .../POSTGRES/code/stress-go-fast/handler.go | 58 +++--- .../code/stress-go-pgstorm/handler.go | 148 +++++++++++++++ examples/POSTGRES/main.tf | 4 +- examples/POSTGRES/resources.tf | 30 +++ examples/TNAR/main.tf | 4 +- examples/demo-managed-functions/variables.tf | 2 +- examples/hello-go/main.tf | 2 +- examples/hello-node/main.tf | 2 +- examples/notes-python/main.tf | 2 +- examples/pg-list-python/main.tf | 2 +- examples/simple-node/main.tf | 2 +- examples/simple-python/main.tf | 2 +- internal/api/handler/invoke.go | 177 ++++++++++-------- internal/builder/context.go | 4 +- runtimes/go1.23/Dockerfile | 42 ++--- runtimes/go1.23/go.mod | 11 ++ runtimes/go1.23/go.sum | 28 +++ 18 files changed, 377 insertions(+), 145 deletions(-) create mode 100644 examples/POSTGRES/code/stress-go-pgstorm/handler.go create mode 100644 runtimes/go1.23/go.sum diff --git a/deployments/k8s/operator.yaml b/deployments/k8s/operator.yaml index 51959bb..467a4c0 100644 --- a/deployments/k8s/operator.yaml +++ b/deployments/k8s/operator.yaml @@ -130,6 +130,8 @@ metadata: cert-manager.io/cluster-issuer: letsencrypt-prod nginx.ingress.kubernetes.io/force-ssl-redirect: "true" nginx.ingress.kubernetes.io/ssl-redirect: "true" + nginx.ingress.kubernetes.io/proxy-read-timeout: "900" + nginx.ingress.kubernetes.io/proxy-send-timeout: "900" spec: ingressClassName: nginx rules: diff --git a/examples/POSTGRES/code/stress-go-fast/handler.go b/examples/POSTGRES/code/stress-go-fast/handler.go index a6e6b5f..cd5ac46 100644 --- a/examples/POSTGRES/code/stress-go-fast/handler.go +++ b/examples/POSTGRES/code/stress-go-fast/handler.go @@ -1,47 +1,43 @@ +package handler // 2026-03-19 // handler.go — быстрая Go функция: факториал + числа Фибоначчи. // Проверяет Go runtime под лёгкой нагрузкой и корректность JSON-ответа. -// // Entrypoint: handler.Handle - package handler import "fmt" -// factorial — рекурсивный факториал (для небольших n <= 20). func factorial(n int) uint64 { -if n <= 1 { -return 1 -} -return uint64(n) * factorial(n-1) + if n <= 1 { + return 1 + } + return uint64(n) * factorial(n-1) } -// fib — итеративное число Фибоначчи. func fib(n int) int { -if n <= 1 { -return n -} -a, b := 0, 1 -for i := 2; i <= n; i++ { -a, b = b, a+b -} -return b + if n <= 1 { + return n + } + a, b := 0, 1 + for i := 2; i <= n; i++ { + a, b = b, a+b + } + return b } -// Handle — точка входа рантайма. func Handle(event map[string]interface{}) interface{} { -n := 10 -if v, ok := event["n"].(float64); ok { -n = int(v) -if n > 20 { -n = 20 -} -} -return map[string]interface{}{ -"runtime": "go1.23", -"version": "v1", -"n": n, -"factorial": fmt.Sprintf("%d", factorial(n)), -"fib": fib(n), -} + n := 10 + if v, ok := event["n"].(float64); ok { + n = int(v) + if n > 20 { + n = 20 + } + } + return map[string]interface{}{ + "runtime": "go1.23", + "version": "v1", + "n": n, + "factorial": fmt.Sprintf("%d", factorial(n)), + "fib": fib(n), + } } diff --git a/examples/POSTGRES/code/stress-go-pgstorm/handler.go b/examples/POSTGRES/code/stress-go-pgstorm/handler.go new file mode 100644 index 0000000..edf4668 --- /dev/null +++ b/examples/POSTGRES/code/stress-go-pgstorm/handler.go @@ -0,0 +1,148 @@ +// 2026-03-19 +// handler.go — Go стресс-тест PostgreSQL через pgxpool. +// Запускает N горутин (default 100), каждая в цикле duration_sec (default 600) +// долбит PG попеременно: INSERT / SELECT COUNT / SELECT MAX с случайными задержками. +// Цель: проверить Go runtime под конкурентной нагрузкой и устойчивость PG connection pool. +// Entrypoint: handler.Handle +package handler + +import ( + "context" + "fmt" + "math/rand" + "os" + "sync" + "sync/atomic" + "time" + + "github.com/jackc/pgx/v5/pgxpool" +) + +// pgDSN собирает DSN из env vars (PGHOST, PGPORT, PGDATABASE, PGUSER, PGPASSWORD, PGSSLMODE). +func pgDSN() string { + host := os.Getenv("PGHOST") + port := os.Getenv("PGPORT") + if port == "" { + port = "5432" + } + db := os.Getenv("PGDATABASE") + user := os.Getenv("PGUSER") + pass := os.Getenv("PGPASSWORD") + sslmode := os.Getenv("PGSSLMODE") + if sslmode == "" { + sslmode = "require" + } + return fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s sslmode=%s", + host, port, db, user, pass, sslmode) +} + +// worker — одна горутина: чередует INSERT/COUNT/MAX с случайной задержкой до maxDelayMs. +// При ошибке инкрементирует errOps и продолжает (не паникует). +func worker(ctx context.Context, pool *pgxpool.Pool, workerID int, maxDelayMs int, okOps, errOps *int64) { + rng := rand.New(rand.NewSource(time.Now().UnixNano() + int64(workerID))) + op := 0 + for { + select { + case <-ctx.Done(): + return + default: + } + + // Случайная задержка перед следующей операцией: 0..maxDelayMs мс + delay := rng.Intn(maxDelayMs + 1) + time.Sleep(time.Duration(delay) * time.Millisecond) + + var err error + switch op % 3 { + case 0: // INSERT + title := fmt.Sprintf("pgstorm-w%d-%d", workerID, time.Now().UnixNano()) + _, err = pool.Exec(ctx, + "INSERT INTO terraform_demo_table (title) VALUES ($1)", title) + case 1: // SELECT COUNT + var count int64 + err = pool.QueryRow(ctx, + "SELECT COUNT(*) FROM terraform_demo_table").Scan(&count) + case 2: // SELECT MAX id + var maxID *int64 + err = pool.QueryRow(ctx, + "SELECT MAX(id) FROM terraform_demo_table").Scan(&maxID) + } + + if err != nil && ctx.Err() == nil { + atomic.AddInt64(errOps, 1) + } else if err == nil { + atomic.AddInt64(okOps, 1) + } + op++ + } +} + +func Handle(event map[string]interface{}) interface{} { + // Параметры из event (все опциональны — разумные defaults) + workers := 100 + if v, ok := event["workers"].(float64); ok && v > 0 && v <= 500 { + workers = int(v) + } + durationSec := 600 + if v, ok := event["duration_sec"].(float64); ok && v > 0 && v <= 3600 { + durationSec = int(v) + } + maxDelayMs := 300 + if v, ok := event["max_delay_ms"].(float64); ok && v >= 0 && v <= 5000 { + maxDelayMs = int(v) + } + + // Инициализация pgxpool — единый pool на всю функцию, MaxConns ограничен + // чтобы не перегрузить managed PG при большом числе горутин. + poolCfg, err := pgxpool.ParseConfig(pgDSN()) + if err != nil { + return map[string]interface{}{"error": fmt.Sprintf("parse dsn: %v", err)} + } + maxConns := 20 + if workers < 20 { + maxConns = workers + } + poolCfg.MaxConns = int32(maxConns) + + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(durationSec)*time.Second) + defer cancel() + + pool, err := pgxpool.NewWithConfig(ctx, poolCfg) + if err != nil { + return map[string]interface{}{"error": fmt.Sprintf("connect pool: %v", err)} + } + defer pool.Close() + + var okOps, errOps int64 + startTime := time.Now() + + var wg sync.WaitGroup + for i := 0; i < workers; i++ { + wg.Add(1) + go func(id int) { + defer wg.Done() + worker(ctx, pool, id, maxDelayMs, &okOps, &errOps) + }(i) + } + wg.Wait() + + elapsed := time.Since(startTime).Seconds() + total := okOps + errOps + opsPerSec := 0.0 + if elapsed > 0 { + opsPerSec = float64(total) / elapsed + } + + return map[string]interface{}{ + "runtime": "go1.23", + "version": "v1", + "workers": workers, + "duration_sec": durationSec, + "max_delay_ms": maxDelayMs, + "elapsed_sec": fmt.Sprintf("%.1f", elapsed), + "total_ops": total, + "ok_ops": okOps, + "err_ops": errOps, + "ops_per_sec": fmt.Sprintf("%.1f", opsPerSec), + } +} diff --git a/examples/POSTGRES/main.tf b/examples/POSTGRES/main.tf index ae97e59..08ab574 100644 --- a/examples/POSTGRES/main.tf +++ b/examples/POSTGRES/main.tf @@ -47,12 +47,12 @@ variable "pg_password" { provider "nubes" { api_token = var.api_token - api_endpoint = "https://deck-api-test.ngcloud.ru/api/v1/index.cfm" + api_endpoint = "https://deck-test.ngcloud.ru/api/v1/index.cfm" } provider "sless" { endpoint = "https://sless.kube5s.ru" token = var.api_token - nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1" + nubes_endpoint = "https://deck-test.ngcloud.ru/api/v1" } diff --git a/examples/POSTGRES/resources.tf b/examples/POSTGRES/resources.tf index 4260672..46afc70 100644 --- a/examples/POSTGRES/resources.tf +++ b/examples/POSTGRES/resources.tf @@ -375,3 +375,33 @@ resource "sless_trigger" "stress_js_badenv_http" { enabled = true } +# --- [9] Go: PG Storm — 100 горутин долбят PostgreSQL напрямую через pgxpool --- +# Тестирует: Go runtime под конкурентной нагрузкой, pgxpool connection pool, +# устойчивость managed PG при массовых INSERT/SELECT. +# Параметры: workers (default 100), duration_sec (default 600), max_delay_ms (default 300). +resource "sless_function" "stress_go_pgstorm" { + name = "stress-go-pgstorm" + runtime = "go1.23" + entrypoint = "handler.Handle" + memory_mb = 256 + timeout_sec = 700 + + env_vars = { + PGHOST = local.pg_host + PGPORT = "5432" + PGDATABASE = local.pg_database + PGUSER = local.pg_username + PGPASSWORD = local.pg_password + PGSSLMODE = "require" + } + + source_dir = "${path.module}/code/stress-go-pgstorm" + depends_on = [sless_job.postgres_table_init_job] +} +resource "sless_trigger" "stress_go_pgstorm_http" { + name = "stress-go-pgstorm-http" + type = "http" + function = sless_function.stress_go_pgstorm.name + enabled = true +} + diff --git a/examples/TNAR/main.tf b/examples/TNAR/main.tf index ae97e59..08ab574 100644 --- a/examples/TNAR/main.tf +++ b/examples/TNAR/main.tf @@ -47,12 +47,12 @@ variable "pg_password" { provider "nubes" { api_token = var.api_token - api_endpoint = "https://deck-api-test.ngcloud.ru/api/v1/index.cfm" + api_endpoint = "https://deck-test.ngcloud.ru/api/v1/index.cfm" } provider "sless" { endpoint = "https://sless.kube5s.ru" token = var.api_token - nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1" + nubes_endpoint = "https://deck-test.ngcloud.ru/api/v1" } diff --git a/examples/demo-managed-functions/variables.tf b/examples/demo-managed-functions/variables.tf index 6012dff..9a3ac2b 100644 --- a/examples/demo-managed-functions/variables.tf +++ b/examples/demo-managed-functions/variables.tf @@ -16,7 +16,7 @@ variable "sless_endpoint" { variable "nubes_endpoint" { description = "Nubes endpoint (нужен провайдеру)" type = string - default = "https://deck-api-test.ngcloud.ru/api/v1" + default = "https://deck-test.ngcloud.ru/api/v1" } variable "pg_dsn" { diff --git a/examples/hello-go/main.tf b/examples/hello-go/main.tf index ef1580e..c432bd4 100644 --- a/examples/hello-go/main.tf +++ b/examples/hello-go/main.tf @@ -13,5 +13,5 @@ terraform { provider "sless" { endpoint = "https://sless.kube5s.ru" token = var.token - nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1" + nubes_endpoint = "https://deck-test.ngcloud.ru/api/v1" } diff --git a/examples/hello-node/main.tf b/examples/hello-node/main.tf index 09f8a39..29d083e 100644 --- a/examples/hello-node/main.tf +++ b/examples/hello-node/main.tf @@ -19,6 +19,6 @@ terraform { provider "sless" { endpoint = "https://sless.kube5s.ru" token = var.token - nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1" + nubes_endpoint = "https://deck-test.ngcloud.ru/api/v1" } diff --git a/examples/notes-python/main.tf b/examples/notes-python/main.tf index 21762bb..674cc14 100644 --- a/examples/notes-python/main.tf +++ b/examples/notes-python/main.tf @@ -23,5 +23,5 @@ terraform { provider "sless" { endpoint = "https://sless.kube5s.ru" token = var.token - nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1" + nubes_endpoint = "https://deck-test.ngcloud.ru/api/v1" } diff --git a/examples/pg-list-python/main.tf b/examples/pg-list-python/main.tf index 871034b..5093bda 100644 --- a/examples/pg-list-python/main.tf +++ b/examples/pg-list-python/main.tf @@ -13,5 +13,5 @@ terraform { provider "sless" { endpoint = "https://sless.kube5s.ru" token = var.token - nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1" + nubes_endpoint = "https://deck-test.ngcloud.ru/api/v1" } diff --git a/examples/simple-node/main.tf b/examples/simple-node/main.tf index af95879..ccf1554 100644 --- a/examples/simple-node/main.tf +++ b/examples/simple-node/main.tf @@ -27,5 +27,5 @@ terraform { provider "sless" { endpoint = "https://sless.kube5s.ru" token = var.token - nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1" + nubes_endpoint = "https://deck-test.ngcloud.ru/api/v1" } diff --git a/examples/simple-python/main.tf b/examples/simple-python/main.tf index e55c11a..02dd45b 100644 --- a/examples/simple-python/main.tf +++ b/examples/simple-python/main.tf @@ -26,5 +26,5 @@ terraform { provider "sless" { endpoint = "https://sless.kube5s.ru" token = var.token - nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1" + nubes_endpoint = "https://deck-test.ngcloud.ru/api/v1" } diff --git a/internal/api/handler/invoke.go b/internal/api/handler/invoke.go index e646a00..bfc4f64 100644 --- a/internal/api/handler/invoke.go +++ b/internal/api/handler/invoke.go @@ -1,4 +1,4 @@ -// Изменено: 2026-03-11 +// Изменено: 2026-03-12 // invoke.go — прокси-обработчик для вызова HTTP-триггеров функций. // Маршрут: ANY /fn/{namespace}/{name} и /fn/{namespace}/{name}/** // Не защищён auth-токеном — это публичный эндпоинт для вызова функций. @@ -6,99 +6,120 @@ // 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 +// Таймаут берётся из Spec.TimeoutSec функции (+ 5s буфер) чтобы не резать +// длительные вызовы (stress-тесты, batch-задачи). package handler import ( - "fmt" - "io" - "net/http" - "strings" - "time" +"fmt" +"io" +"net/http" +"strings" +"time" - "github.com/gorilla/mux" +"github.com/gorilla/mux" +slessv1alpha1 "gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/api/v1alpha1" +"sigs.k8s.io/controller-runtime/pkg/client" ) -// httpClient используется для обращения к функциям внутри кластера. -// Таймаут 30s — достаточно для холодного старта функции. -var httpClient = &http.Client{Timeout: 30 * time.Second} - // hopByHopHeaders — заголовки которые нельзя пробрасывать через прокси (RFC 2616 §13.5.1). // Они управляют соединением между двумя узлами, а не end-to-end. // Особо опасен Transfer-Encoding: если пробросить его, клиент неверно интерпретирует тело. var hopByHopHeaders = map[string]bool{ - "Connection": true, - "Keep-Alive": true, - "Proxy-Authenticate": true, - "Proxy-Authorization": true, - "Te": true, - "Trailers": true, - "Transfer-Encoding": true, - "Upgrade": true, +"Connection": true, +"Keep-Alive": true, +"Proxy-Authenticate": true, +"Proxy-Authorization": true, +"Te": true, +"Trailers": true, +"Transfer-Encoding": true, +"Upgrade": true, +} + +// invokeHTTPClient создаёт http.Client с таймаутом под конкретный вызов. +// timeout = TimeoutSec функции + 5s буфер на сетевые задержки. +// Если TimeoutSec == 0 (не задан), используем 30s по умолчанию. +func invokeHTTPClient(timeoutSec int32) *http.Client { +t := time.Duration(timeoutSec)*time.Second + 5*time.Second +if timeoutSec <= 0 { +t = 30 * time.Second +} +return &http.Client{Timeout: t} } // InvokeFunction проксирует входящий запрос к Service функции в кластере. // Namespace выбирается из пути, имя функции — тоже из пути. // Сохраняет метод, тело, Content-Type, sub-path и query string. +// Таймаут прокси-клиента = Spec.TimeoutSec функции + 5s (резинка). func (h *Handler) InvokeFunction(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - ns := vars["namespace"] - name := vars["name"] +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 и Content-Length если есть. - // Content-Length обязателен: Python BaseHTTPRequestHandler читает тело - // ровно столько байт, сколько указано в заголовке; без него body = пусто. - if ct := r.Header.Get("Content-Type"); ct != "" { - proxyReq.Header.Set("Content-Type", ct) - } - proxyReq.ContentLength = r.ContentLength - - resp, err := httpClient.Do(proxyReq) - if err != nil { - // "no such host" — Service не существует (функция удалена), возвращаем 404. - // Это отличается от временной сетевой ошибки: NXDOMAIN строго означает отсутствие записи. - if strings.Contains(err.Error(), "no such host") { - writeJSON(w, http.StatusNotFound, errResp("function not found")) - return - } - 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() - - // Копируем заголовки и статус из ответа функции. - // Hop-by-hop заголовки фильтруем: они управляют конкретным TCP-соединением - // и не должны пробрасываться через прокси (RFC 2616 §13.5.1). - for k, vals := range resp.Header { - if hopByHopHeaders[k] { - continue - } - for _, v := range vals { - w.Header().Add(k, v) - } - } - w.WriteHeader(resp.StatusCode) - _, _ = io.Copy(w, resp.Body) +// Смотрим TimeoutSec из Function CRD, чтобы не резать длительные вызовы. +// Если функция не найдена — продолжаем с дефолтным таймаутом (30s). +var timeoutSec int32 +fn := &slessv1alpha1.Function{} +if err := h.K8s.Get(r.Context(), client.ObjectKey{Name: name, Namespace: ns}, fn); err == nil { +timeoutSec = fn.Spec.TimeoutSec +} +httpClient := invokeHTTPClient(timeoutSec) + +// Вычисляем 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 и Content-Length если есть. +// Content-Length обязателен: Python BaseHTTPRequestHandler читает тело +// ровно столько байт, сколько указано в заголовке; без него body = пусто. +if ct := r.Header.Get("Content-Type"); ct != "" { +proxyReq.Header.Set("Content-Type", ct) +} +proxyReq.ContentLength = r.ContentLength + +resp, err := httpClient.Do(proxyReq) +if err != nil { +// "no such host" — Service не существует (функция удалена), возвращаем 404. +// Это отличается от временной сетевой ошибки: NXDOMAIN строго означает отсутствие записи. +if strings.Contains(err.Error(), "no such host") { +writeJSON(w, http.StatusNotFound, errResp("function not found")) +return +} +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() + +// Копируем заголовки и статус из ответа функции. +// Hop-by-hop заголовки фильтруем: они управляют конкретным TCP-соединением +// и не должны пробрасываться через прокси (RFC 2616 §13.5.1). +for k, vals := range resp.Header { +if hopByHopHeaders[k] { +continue +} +for _, v := range vals { +w.Header().Add(k, v) +} +} +w.WriteHeader(resp.StatusCode) +_, _ = io.Copy(w, resp.Body) } diff --git a/internal/builder/context.go b/internal/builder/context.go index c510ea5..52270ee 100644 --- a/internal/builder/context.go +++ b/internal/builder/context.go @@ -70,9 +70,9 @@ func runtimeBaseImage(runtime string) (string, error) { case "nodejs20": return "naeel/sless-runtime-nodejs20:v0.1.2", nil case "go1.23": - // Go использует builder-образ (содержит golang:1.23 + server.go). + // Go использует builder-образ (содержит golang:1.23 + server.go + go.mod/go.sum с pgx/v5). // Конечный образ multi-stage — alpine без компилятора. - return "naeel/sless-runtime-go1.23:v0.1.0", nil + return "naeel/sless-runtime-go1.23:v0.1.1", nil default: return "", fmt.Errorf("unsupported runtime: %q (supported: python3.11, nodejs20, go1.23)", runtime) } diff --git a/runtimes/go1.23/Dockerfile b/runtimes/go1.23/Dockerfile index 8bc3e0f..9258a19 100644 --- a/runtimes/go1.23/Dockerfile +++ b/runtimes/go1.23/Dockerfile @@ -1,33 +1,29 @@ -# Создано: 2026-03-11 +# Изменено: 2026-03-19 — v0.1.1: добавлены go.sum + go mod download (pgx/v5 v5.7.2). # Base builder image для Go 1.23 serverless функций. -# Это BUILDER-образ (не runtime): содержит Go компилятор + server.go. -# server.go — HTTP-wrapper + job-runner, компилируется ВМЕСТЕ с кодом пользователя. +# Это BUILDER-образ: golang:1.23-alpine + server.go + pre-cached зависимости. +# go mod download кеширует pgx/v5 в /root/go/pkg/mod — kaniko не скачивает их при каждой сборке. # -# kaniko делает multi-stage build: -# Stage 1: golang:1.23-alpine → COPY server.go + handler/ → go build → /server -# Stage 2: FROM alpine → COPY /server → минимальный финальный образ. +# kaniko генерирует Dockerfile для каждой функции: +# FROM naeel/sless-runtime-go1.23:v0.1.1 AS builder +# WORKDIR /app +# COPY . /app/handler/ +# RUN CGO_ENABLED=0 go build -o /server . +# FROM alpine:3.20 +# COPY --from=builder /server /server → финальный образ # -# Почему builder-образ, а не runtime: Go требует статической компиляции — нельзя -# "подключить" пользовательский код динамически как в Python/Node. +# Почему не multi-stage здесь: base образ должен иметь Go компилятор и module cache. +# Бинарник собирается kaniko, а не здесь. -FROM golang:1.23-alpine AS builder +FROM golang:1.23-alpine WORKDIR /app -# server.go — точка входа (main package), вызывает Handler() из пользовательского кода. +# Копируем mod файлы первыми — отдельный кешируемый слой. +# При изменении go.mod/go.sum слой инвалидируется и go mod download запускается заново. +COPY go.mod go.sum /app/ +RUN go mod download + +# server.go — HTTP-wrapper + job-runner (main package, импортирует sless/fn/handler). COPY server.go /app/server.go -# Код пользователя kaniko копирует в /app/handler/ -COPY handler/ /app/handler/ - -# Статическая сборка (без libc) — работает в alpine-контейнере -RUN CGO_ENABLED=0 go build -o /server /app/server.go - -# Финальный минимальный образ -FROM alpine:3.20 - -COPY --from=builder /server /server - EXPOSE 8080 - -CMD ["/server"] diff --git a/runtimes/go1.23/go.mod b/runtimes/go1.23/go.mod index 9c6f5c5..5005d86 100644 --- a/runtimes/go1.23/go.mod +++ b/runtimes/go1.23/go.mod @@ -1,3 +1,14 @@ module sless/fn go 1.23 + +require github.com/jackc/pgx/v5 v5.7.2 + +require ( + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/text v0.21.0 // indirect +) diff --git a/runtimes/go1.23/go.sum b/runtimes/go1.23/go.sum new file mode 100644 index 0000000..731b5df --- /dev/null +++ b/runtimes/go1.23/go.sum @@ -0,0 +1,28 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= +github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=