feat: Go runtime v0.1.1 (pgx/v5), stress-go-pgstorm, fix invoke.go dynamic timeout, nginx ingress timeout 900s

- 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)
This commit is contained in:
Naeel
2026-03-19 21:33:41 +03:00
parent 8dc07445ac
commit d7fda15d35
18 changed files with 377 additions and 145 deletions
+2
View File
@@ -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:
@@ -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),
}
}
@@ -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),
}
}
+2 -2
View File
@@ -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"
}
+30
View File
@@ -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
}
+2 -2
View File
@@ -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"
}
+1 -1
View File
@@ -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" {
+1 -1
View File
@@ -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"
}
+1 -1
View File
@@ -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"
}
+1 -1
View File
@@ -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"
}
+1 -1
View File
@@ -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"
}
+1 -1
View File
@@ -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"
}
+1 -1
View File
@@ -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"
}
+99 -78
View File
@@ -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)
}
+2 -2
View File
@@ -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)
}
+19 -23
View File
@@ -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"]
+11
View File
@@ -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
)
+28
View File
@@ -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=