diff --git a/.gitignore b/.gitignore index fda4c08..a3c5f91 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ build/ # Secrets secrets/ goaws +cmd diff --git a/app/admin/admin.go b/app/admin/admin.go index d514567..faaf3aa 100644 --- a/app/admin/admin.go +++ b/app/admin/admin.go @@ -1,7 +1,7 @@ // app/admin/admin.go // Admin API handlers for shared-sqs management // Created: 2026-04-09 -// Updated: 2026-04-12 09:28 MSK — demo UI token и изоляция UI API одним tenant-ом +// Updated: 2026-04-12 10:12 MSK — пометки о временном demo showcase режиме package admin import ( @@ -30,6 +30,9 @@ const ( uiTenantContextKey uiContextKey = "ui-tenant" ) +// ВАЖНО: значения ниже относятся только к временному demo/showcase режиму. +// Перед production rollout без публичного demo-режима этот блок должен быть удалён +// вместе с веткой authenticateUIDemoToken и связанными UI-подсказками. const ( defaultUIDemoToken = "demo-ui-shared-sqs-ngcloud-2026" defaultUIDemoTenantID = "t-demo-shared-sqs-ngcloud" @@ -67,7 +70,8 @@ func NewHandler(store *tenant.TenantStore, adminToken string) *Handler { return &Handler{store: store, adminToken: adminToken, nubesEndpoint: nubesEndpoint} } -// uiDemoToken — возвращает публичный demo token для UI, если он не переопределён через env. +// uiDemoToken — возвращает публичный demo token для UI. +// Временный showcase-only путь: в production без demo user этот метод нужно удалить. func (h *Handler) uiDemoToken() string { if token := os.Getenv("SHARED_SQS_UI_DEMO_TOKEN"); token != "" { return token @@ -76,6 +80,7 @@ func (h *Handler) uiDemoToken() string { } // uiDemoTenantID — возвращает tenant ID, к которому привязан demo token. +// Существует только для showcase-режима, не для постоянной production auth-модели. func (h *Handler) uiDemoTenantID() string { if tenantID := os.Getenv("SHARED_SQS_UI_DEMO_TENANT_ID"); tenantID != "" { return tenantID @@ -84,6 +89,7 @@ func (h *Handler) uiDemoTenantID() string { } // uiDemoEmail — возвращает отображаемый email для demo UI session. +// Нужен только для публичного демо-логина и должен уйти вместе с demo path. func (h *Handler) uiDemoEmail() string { if email := os.Getenv("SHARED_SQS_UI_DEMO_EMAIL"); email != "" { return email @@ -92,6 +98,8 @@ func (h *Handler) uiDemoEmail() string { } // authenticateUIDemoToken — маппит публичный demo token на заранее сидированный demo tenant. +// Почему так: это быстрый showcase-вход для заказчика. Для production без demo user +// функция должна быть удалена, чтобы в коде не осталось публичного bypass-пути. func (h *Handler) authenticateUIDemoToken(token string) (*tenant.Tenant, string, error) { demoToken := h.uiDemoToken() if demoToken == "" || subtle.ConstantTimeCompare([]byte(token), []byte(demoToken)) != 1 { diff --git a/app/billing/billing.go b/app/billing/billing.go new file mode 100644 index 0000000..5b5046c --- /dev/null +++ b/app/billing/billing.go @@ -0,0 +1,126 @@ +// app/billing/billing.go +// Модуль учёта использования SQS-операций для биллинга. +// Записывает каждую успешную SQS-операцию в PostgreSQL: tenant_id, operation, msg_count, msg_bytes. +// Если PostgreSQL не сконфигурирован — billing отключён, SQS работает как раньше. +// Created: 2026-04-12 +package billing + +import ( + "database/sql" + "fmt" + "os" + + // PostgreSQL драйвер — регистрируется в database/sql через init() + _ "github.com/lib/pq" + log "github.com/sirupsen/logrus" +) + +// db — подключение к PostgreSQL для записи usage-данных. +// nil если billing отключён. +var db *sql.DB + +// Init — подключается к PostgreSQL и создаёт таблицу sqs_usage_records если не существует. +// Env переменные: BILLING_PG_HOST, BILLING_PG_PORT, BILLING_PG_DATABASE, BILLING_PG_USER, +// BILLING_PG_PASSWORD, BILLING_PG_SSLMODE. +// Если BILLING_PG_HOST не задан — billing отключён, сервис работает без него. +func Init() { + host := os.Getenv("BILLING_PG_HOST") + if host == "" { + log.Info("billing: BILLING_PG_HOST not set, usage tracking disabled") + return + } + + port := os.Getenv("BILLING_PG_PORT") + if port == "" { + port = "5432" + } + dbname := os.Getenv("BILLING_PG_DATABASE") + user := os.Getenv("BILLING_PG_USER") + password := os.Getenv("BILLING_PG_PASSWORD") + sslmode := os.Getenv("BILLING_PG_SSLMODE") + if sslmode == "" { + sslmode = "require" + } + + dsn := fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s sslmode=%s", + host, port, dbname, user, password, sslmode) + + var err error + db, err = sql.Open("postgres", dsn) + if err != nil { + log.Errorf("billing: failed to open PostgreSQL: %v", err) + return + } + + // Проверяем реальное подключение (Open не подключается) + if err = db.Ping(); err != nil { + log.Errorf("billing: failed to connect to PostgreSQL: %v", err) + db.Close() + db = nil + return + } + + // Ограничиваем пул — billing не должен отжирать коннекты у основной БД + db.SetMaxOpenConns(5) + db.SetMaxIdleConns(2) + + if err = autoMigrate(); err != nil { + log.Errorf("billing: failed to create table: %v", err) + db.Close() + db = nil + return + } + + log.Infof("billing: connected to PostgreSQL %s:%s/%s, usage tracking enabled", host, port, dbname) +} + +// autoMigrate — создаёт таблицу и индекс если не существуют. +// Идемпотентно — безопасно вызывать при каждом старте. +func autoMigrate() error { + _, err := db.Exec(` + CREATE TABLE IF NOT EXISTS sqs_usage_records ( + id BIGSERIAL PRIMARY KEY, + tenant_id TEXT NOT NULL, + operation TEXT NOT NULL, + msg_count INTEGER DEFAULT 1, + msg_bytes BIGINT DEFAULT 0, + recorded_at TIMESTAMPTZ DEFAULT NOW() + ); + CREATE INDEX IF NOT EXISTS idx_sqs_usage_tenant_time + ON sqs_usage_records(tenant_id, recorded_at); + `) + return err +} + +// RecordUsage — записывает одну SQS-операцию в таблицу биллинга. +// Вызывается асинхронно (горутина) чтобы не добавлять latency к SQS-ответу. +// Если billing отключён — no-op. Ошибки логируются, SQS-операция не ломается. +func RecordUsage(tenantID, operation string, msgCount int, msgBytes int64) { + if db == nil { + return + } + + go func() { + _, err := db.Exec( + `INSERT INTO sqs_usage_records (tenant_id, operation, msg_count, msg_bytes) VALUES ($1, $2, $3, $4)`, + tenantID, operation, msgCount, msgBytes, + ) + if err != nil { + log.Errorf("billing: failed to record usage [%s/%s]: %v", tenantID, operation, err) + } + }() +} + +// Enabled — возвращает true если billing подключён к PostgreSQL +func Enabled() bool { + return db != nil +} + +// Close — закрывает подключение к PostgreSQL. Вызывается при graceful shutdown. +func Close() { + if db != nil { + db.Close() + db = nil + log.Info("billing: PostgreSQL connection closed") + } +} diff --git a/app/cmd/goaws.go b/app/cmd/goaws.go index d097e43..ee64df1 100644 --- a/app/cmd/goaws.go +++ b/app/cmd/goaws.go @@ -13,6 +13,7 @@ import ( "syscall" "time" + "shared-sqs/app/billing" "shared-sqs/app/conf" "shared-sqs/app/gosqs" "shared-sqs/app/models" @@ -124,6 +125,10 @@ func main() { if os.Getenv("SHARED_SQS_SEED_DEMO") == "true" { seedDemoData(tenantStore) } + + // Billing: подключение к PostgreSQL для учёта использования. + // Если BILLING_PG_HOST не задан — billing отключён, SQS работает без него. + billing.Init() // Роутер с tenant auth и admin API r := router.New(tenantStore, adminToken) @@ -163,6 +168,9 @@ func main() { // Остановить PeriodicTasks close(quit) + // Закрыть billing (если был подключён) + billing.Close() + // Дать 10 секунд на завершение текущих HTTP запросов ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() diff --git a/app/router/router.go b/app/router/router.go index ebcd144..8f8d71e 100644 --- a/app/router/router.go +++ b/app/router/router.go @@ -13,6 +13,7 @@ import ( "shared-sqs/app/admin" "shared-sqs/app/auth" + "shared-sqs/app/billing" sqs "shared-sqs/app/gosqs" "shared-sqs/app/interfaces" "shared-sqs/app/tenant" @@ -131,6 +132,16 @@ func actionHandler(w http.ResponseWriter, req *http.Request) { if ok { statusCode, responseBody := jsonFn(req) encodeResponse(w, req, statusCode, responseBody) + // Billing: записываем каждую успешную SQS-операцию (async, no-op если billing выключен) + if statusCode < 400 { + if t, _ := req.Context().Value(auth.TenantContextKey).(*tenant.Tenant); t != nil { + msgBytes := req.ContentLength + if msgBytes < 0 { + msgBytes = 0 + } + billing.RecordUsage(t.ID, action, 1, msgBytes) + } + } return } log.Warnf("Bad Request - Action: %s", action) diff --git a/deployments/helm/shared-sqs/templates/deployment.yaml b/deployments/helm/shared-sqs/templates/deployment.yaml new file mode 100644 index 0000000..acfdd20 --- /dev/null +++ b/deployments/helm/shared-sqs/templates/deployment.yaml @@ -0,0 +1,97 @@ +# deployment.yaml — Deployment shared-sqs с RollingUpdate +# Created: 2026-04-11 +apiVersion: apps/v1 +kind: Deployment +metadata: + name: shared-sqs + namespace: {{ .Values.namespace }} + labels: + {{- include "shared-sqs.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + strategy: + type: {{ .Values.strategy.type }} + {{- if eq .Values.strategy.type "RollingUpdate" }} + rollingUpdate: + maxSurge: {{ .Values.strategy.rollingUpdate.maxSurge }} + maxUnavailable: {{ .Values.strategy.rollingUpdate.maxUnavailable }} + {{- end }} + selector: + matchLabels: + {{- include "shared-sqs.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "shared-sqs.selectorLabels" . | nindent 8 }} + spec: + containers: + - name: shared-sqs + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - containerPort: {{ .Values.service.port }} + name: http + env: + - name: SHARED_SQS_ADMIN_TOKEN + valueFrom: + secretKeyRef: + name: shared-sqs-admin + key: token + - name: SHARED_SQS_SEED_DEMO + value: {{ .Values.seedDemo | quote }} + - name: REDIS_ADDR + valueFrom: + secretKeyRef: + name: shared-sqs-redis + key: addr + - name: REDIS_USER + valueFrom: + secretKeyRef: + name: shared-sqs-redis + key: user + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: shared-sqs-redis + key: password + - name: NUBES_ENDPOINT + value: {{ .Values.nubesEndpoint | quote }} + {{- if .Values.billing.enabled }} + - name: BILLING_PG_HOST + value: {{ .Values.billing.postgres.host | quote }} + - name: BILLING_PG_PORT + value: {{ .Values.billing.postgres.port | quote }} + - name: BILLING_PG_DATABASE + value: {{ .Values.billing.postgres.database | quote }} + - name: BILLING_PG_USER + value: {{ .Values.billing.postgres.user | quote }} + - name: BILLING_PG_SSLMODE + value: {{ .Values.billing.postgres.sslmode | quote }} + - name: BILLING_PG_PASSWORD + valueFrom: + secretKeyRef: + {{- if .Values.billing.postgres.existingSecret }} + name: {{ .Values.billing.postgres.existingSecret }} + {{- else }} + name: shared-sqs-billing-pg + {{- end }} + key: {{ .Values.billing.postgres.secretKey | default "password" }} + {{- end }} + resources: + {{- toYaml .Values.resources | nindent 10 }} + livenessProbe: + httpGet: + path: {{ .Values.probes.liveness.path }} + port: {{ .Values.service.port }} + initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }} + periodSeconds: {{ .Values.probes.liveness.periodSeconds }} + readinessProbe: + httpGet: + path: {{ .Values.probes.readiness.path }} + port: {{ .Values.service.port }} + initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }} + periodSeconds: {{ .Values.probes.readiness.periodSeconds }} + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/deployments/helm/shared-sqs/templates/secret-billing.yaml b/deployments/helm/shared-sqs/templates/secret-billing.yaml new file mode 100644 index 0000000..884831d --- /dev/null +++ b/deployments/helm/shared-sqs/templates/secret-billing.yaml @@ -0,0 +1,15 @@ +# secret-billing.yaml — Secret с PostgreSQL credentials для billing +# Создаётся только если billing.enabled=true и existingSecret не указан +# Created: 2026-04-12 +{{- if and .Values.billing.enabled (not .Values.billing.postgres.existingSecret) }} +apiVersion: v1 +kind: Secret +metadata: + name: shared-sqs-billing-pg + namespace: {{ .Values.namespace }} + labels: + {{- include "shared-sqs.labels" . | nindent 4 }} +type: Opaque +stringData: + password: {{ required "billing.postgres.password is required when billing is enabled" .Values.billing.postgres.password | quote }} +{{- end }} diff --git a/deployments/helm/shared-sqs/values.yaml b/deployments/helm/shared-sqs/values.yaml new file mode 100644 index 0000000..533bbbf --- /dev/null +++ b/deployments/helm/shared-sqs/values.yaml @@ -0,0 +1,97 @@ +# values.yaml — параметры деплоя shared-sqs +# Created: 2026-04-11 +# Updated: 2026-04-12 09:57 MSK — image tag v0.1.22 для demo UI showcase mode +# Все значения — переменные, меняются при деплое через --set или -f override.yaml + +# -- Namespace для деплоя +namespace: shared-sqs + +# -- Количество реплик +replicaCount: 1 + +# -- Образ приложения +image: + repository: naeel/shared-sqs + tag: "v0.1.22" + pullPolicy: IfNotPresent + +# -- imagePullSecrets (имя Secret'а с credentials к registry) +imagePullSecrets: + - name: sless-registry-auth + +# -- Redis: эндпоинт, credentials +# addr — полный host:port (можно internal K8s DNS или внешний IP) +redis: + addr: "rfrm-redisk8s.54467f74-67f5-4ca7-9b4a-bdd50e8c23d6.svc:6379" + user: "default" + password: "quiY2oovugaiChiejah9" + +# -- Admin токен для /admin/* API +admin: + token: "sqs-admin-7a7d8bd0c060a75c198d48680f34077a" + +# -- Seed demo tenant при старте +seedDemo: true + +# -- Nubes API endpoint +nubesEndpoint: "https://deck-api-test.ngcloud.ru/api/v1" + +# -- Ресурсы контейнера +resources: + requests: + memory: "64Mi" + cpu: "50m" + limits: + memory: "256Mi" + cpu: "500m" + +# -- Service +service: + type: ClusterIP + port: 4100 + +# -- Ingress +ingress: + enabled: true + className: nginx + host: qu.kube5s.ru + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + nginx.ingress.kubernetes.io/proxy-body-size: "10m" + nginx.ingress.kubernetes.io/proxy-read-timeout: "30" + nginx.ingress.kubernetes.io/proxy-send-timeout: "30" + tls: + enabled: true + secretName: shared-sqs-tls + +# -- Probes +probes: + liveness: + path: /health + initialDelaySeconds: 5 + periodSeconds: 10 + readiness: + path: /health + initialDelaySeconds: 3 + periodSeconds: 5 + +# -- Strategy +strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + +# -- Billing: PostgreSQL для учёта использования SQS-операций. +# Если enabled=false — billing отключён, сервис работает без PostgreSQL. +billing: + enabled: false + postgres: + host: "" + port: "5432" + database: "sqsdb" + user: "" + sslmode: "require" + # Имя существующего K8s Secret с ключом "password" + existingSecret: "" + secretKey: "password" diff --git a/doc/decisions/demo-ui-showcase-2026-04-12.md b/doc/decisions/demo-ui-showcase-2026-04-12.md index a30e4e2..ecd0a30 100644 --- a/doc/decisions/demo-ui-showcase-2026-04-12.md +++ b/doc/decisions/demo-ui-showcase-2026-04-12.md @@ -33,4 +33,15 @@ - Решение предназначено для demo/showcase, не для production security model. - В production demo token должен быть удалён вместе с seeded demo tenant. -- Основным постоянным сценарием остаётся вход по реальному Nubes JWT. \ No newline at end of file +- Основным постоянным сценарием остаётся вход по реальному Nubes JWT. + +## Что удалить перед production без demo user + +- Ветку `authenticateUIDemoToken` и связанные demo-константы в [app/admin/admin.go](/home/naeel/remote_dev/SQS-service/app/admin/admin.go) +- Demo token и demo-подсказки из [app/ui/index.html](/home/naeel/remote_dev/SQS-service/app/ui/index.html) +- Seed demo tenant и его тестовые очереди/сообщения +- Публичные demo credentials из пользовательских README/инструкций + +## Что лучше, если demo path может жить дольше + +Если demo-режим понадобится и после первой презентации, лучший следующий шаг — не держать его как "просто ещё один путь", а вынести под отдельный явный feature flag с default=off для production. Тогда риски забыть demo bypass в боевом окружении будут существенно ниже. \ No newline at end of file diff --git a/doc/progress.md b/doc/progress.md index da19247..950d519 100644 --- a/doc/progress.md +++ b/doc/progress.md @@ -2,6 +2,15 @@ ## Версия v0.1.x +### v0.1.23-dev (2026-04-12) — billing: учёт SQS-операций в PostgreSQL +- ✅ Новый пакет `app/billing/` — подключение к PG, auto-migrate, async запись usage +- ✅ Интеграция в actionHandler — каждая успешная SQS-операция записывается +- ✅ Опциональность: если BILLING_PG_HOST не задан — billing выключен, SQS работает как раньше +- ✅ Helm chart: секция `billing:` в values.yaml, secret-billing.yaml, env в deployment.yaml +- ✅ Таблица `sqs_usage_records`: tenant_id, operation, msg_count, msg_bytes, recorded_at +- ✅ `go build ./...` — OK +- 🔲 Собрать Docker image и задеплоить с PostgreSQL credentials + ### v0.1.22 (2026-04-12) — demo UI showcase mode deployed - ✅ Demo UI token поддержан сервером: `demo-ui-shared-sqs-ngcloud-2026` - ✅ Реальный JWT login сохранён без изменений @@ -122,6 +131,8 @@ ## Next Steps +- [ ] Перед production без demo user удалить из кода demo UI token path, seeded demo tenant, demo credentials в README и все публичные demo-подсказки в UI +- [ ] Если demo path нужен дольше, сначала вынести его под явный feature flag с default=off для production окружения - [ ] Per-queue locking (заменить глобальный мьютекс на per-queue sync.RWMutex) - [ ] DLQ (Dead Letter Queue) — maxReceiveCount → перемещение в DLQ - [ ] Rate limiting per tenant @@ -187,6 +198,12 @@ Secret Key: demo-secret-key-shared-sqs-ngcloud-2026 Endpoint: https://qu.kube5s.ru ``` +TODO перед production без demo-доступа: +- удалить demo token path из [app/admin/admin.go](/home/naeel/remote_dev/SQS-service/app/admin/admin.go) +- выключить/удалить seeded demo tenant +- удалить публичные demo credentials и demo token из пользовательской документации +- убрать demo-подсказки из [app/ui/index.html](/home/naeel/remote_dev/SQS-service/app/ui/index.html) + ## Deployment ```bash diff --git a/doc/thinking/2026-04-12.md b/doc/thinking/2026-04-12.md index 78688eb..e60daff 100644 --- a/doc/thinking/2026-04-12.md +++ b/doc/thinking/2026-04-12.md @@ -389,4 +389,46 @@ - вопрос прикладной конкурентоспособности shared-sqs в practically useful диапазоне до `32KB`; - отдельную transport/platform проблему `64KB+`, уже локализованную на ingress path. +--- + +# Agent: GitHub Copilot (Claude Opus 4.6) + +## Billing: учёт использования SQS-операций в PostgreSQL + +### Контекст +Пользователь решил добавить billing-учёт в shared-sqs. Задача сервиса — только собирать данные (tenant, операция, количество, объём). Подсчёт денег — отдельный биллинг-сервис. + +### Решения (согласованы с пользователем) +1. **Одна таблица** `sqs_usage_records` — не по тенанту. PostgreSQL держит сотни миллионов строк с индексом. +2. **Строка на каждый API-вызов** — без агрегации. Место дешёвое. +3. **PostgreSQL** — внешний инстанс IoT-PG (iot-naeel realm), база `sqsdb`, юзер `super`. +4. **Опциональность** — если `BILLING_PG_HOST` не задан, billing отключён, SQS работает как раньше. +5. **При старте** — auto-migrate: CREATE TABLE IF NOT EXISTS + индекс. +6. **Helm chart** — секция `billing:` с enabled/postgres параметрами. + +### Реализация +- Новый пакет `app/billing/billing.go`: + - `Init()` — подключение к PG, auto-migrate, пул 5 коннектов + - `RecordUsage(tenantID, operation, msgCount, msgBytes)` — async INSERT через горутину + - `Close()` — graceful shutdown + - Если PG недоступен — лог ошибки, SQS продолжает работать +- Интеграция в `router.go` → `actionHandler()` — единая точка для ВСЕХ SQS-операций + - Записывается только при statusCode < 400 (успешные операции) + - tenantID из request context, operation из action string, msg_bytes из Content-Length +- `goaws.go` (main) — `billing.Init()` при старте, `billing.Close()` при shutdown +- Helm: `values.yaml` (billing section), `secret-billing.yaml`, `deployment.yaml` (env vars) + +### Схема таблицы +```sql +sqs_usage_records ( + id BIGSERIAL PK, + tenant_id TEXT NOT NULL, + operation TEXT NOT NULL, + msg_count INTEGER DEFAULT 1, + msg_bytes BIGINT DEFAULT 0, + recorded_at TIMESTAMPTZ DEFAULT NOW() +) +INDEX: idx_sqs_usage_tenant_time (tenant_id, recorded_at) +``` + Именно такое разделение и нужно, чтобы дальше не смешивать хорошие рабочие метрики сервиса с чужим инфраструктурным ограничением. \ No newline at end of file diff --git a/go.mod b/go.mod index 811044c..720db57 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,9 @@ require ( github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.0 github.com/gorilla/schema v1.4.1 + github.com/lib/pq v1.12.3 github.com/mitchellh/copystructure v1.2.0 + github.com/redis/go-redis/v9 v9.18.0 github.com/sirupsen/logrus v1.9.0 github.com/stretchr/testify v1.7.0 ) @@ -19,7 +21,6 @@ require ( github.com/kr/pretty v0.1.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/redis/go-redis/v9 v9.18.0 // indirect go.uber.org/atomic v1.11.0 // indirect golang.org/x/sys v0.13.0 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect diff --git a/go.sum b/go.sum index 35a21dd..9a9706c 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -13,11 +17,15 @@ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E= github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= +github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= +github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= @@ -31,6 +39,8 @@ github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=