Files
edgeguard-native/internal/services/backends/backends.go
Debian 0ac91a7c59 feat: per-Backend timeout server + Alerts-Deeplink + Retention + Cert-Prune — v1.3.4
- feat(backends): per-Backend `server_timeout_seconds` (nullable, Default 60s).
  Rendert `timeout server <N>s` im HAProxy-Backend-Block — für langsam
  antwortende Upstreams (KI-/Inferenz-Server mit gepufferter Antwort).
  Migration 0044 (+CHECK 1..86400), Model/Repo/Template/UI + Render-Test.
- fix(ui): Dashboard-Alert-Karte verlinkt auf /alerts?tab=events; Alerts-Seite
  respektiert ?tab= (Deeplink landete bisher auf leerem Channels-Tab).
- feat(scheduler): alert_events-Retention (90d) im täglichen Cleanup-Tick —
  Schutz vor unbounded growth der node-lokalen Health-Event-History.
- fix(cluster): Cert-Sync prunt jetzt lokale .pem die der Primary nicht mehr
  hat (Waisen gelöschter Domains); schützt _default.pem + eigenen Node-Cert.
- fix(security): x/text v0.37→v0.39 (GO-2026-5970, Infinite-Loop; via ACME+goose
  aktiv aufgerufen — govulncheck-Release-Gate).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 15:59:55 +02:00

140 lines
3.7 KiB
Go

// Package backends implements CRUD against the `backends` table.
// Ein Backend ist ein Pool — Name, Scheme, Healthcheck, LB-Algorithm,
// WebSocket-Flag. Die konkreten Upstream-Server liegen in
// backend_servers (siehe services/backendservers).
package backends
import (
"context"
"errors"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"git.netcell-it.de/projekte/edgeguard-native/internal/models"
)
var (
ErrNotFound = errors.New("backend not found")
ErrInUse = errors.New("backend is referenced by one or more routing rules")
)
type Repo struct {
Pool *pgxpool.Pool
}
func New(pool *pgxpool.Pool) *Repo { return &Repo{Pool: pool} }
const baseSelect = `
SELECT id, name, scheme, health_check_path, lb_algorithm, websocket, force_http1,
server_timeout_seconds, active, created_at, updated_at
FROM backends
`
func (r *Repo) List(ctx context.Context) ([]models.Backend, error) {
rows, err := r.Pool.Query(ctx, baseSelect+" ORDER BY name ASC")
if err != nil {
return nil, err
}
defer rows.Close()
out := make([]models.Backend, 0, 16)
for rows.Next() {
b, err := scanBackend(rows)
if err != nil {
return nil, err
}
out = append(out, *b)
}
return out, rows.Err()
}
func (r *Repo) Get(ctx context.Context, id int64) (*models.Backend, error) {
row := r.Pool.QueryRow(ctx, baseSelect+" WHERE id = $1", id)
b, err := scanBackend(row)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrNotFound
}
return nil, err
}
return b, nil
}
func (r *Repo) Create(ctx context.Context, b models.Backend) (*models.Backend, error) {
if b.LBAlgorithm == "" {
b.LBAlgorithm = "roundrobin"
}
row := r.Pool.QueryRow(ctx, `
INSERT INTO backends (name, scheme, health_check_path, lb_algorithm, websocket, force_http1,
server_timeout_seconds, active)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id, name, scheme, health_check_path, lb_algorithm, websocket, force_http1,
server_timeout_seconds, active, created_at, updated_at`,
b.Name, b.Scheme, b.HealthCheckPath, b.LBAlgorithm, b.WebSocket, b.ForceHTTP1,
b.ServerTimeoutSeconds, b.Active)
return scanBackend(row)
}
func (r *Repo) Update(ctx context.Context, id int64, b models.Backend) (*models.Backend, error) {
if b.LBAlgorithm == "" {
b.LBAlgorithm = "roundrobin"
}
row := r.Pool.QueryRow(ctx, `
UPDATE backends SET
name = $1,
scheme = $2,
health_check_path = $3,
lb_algorithm = $4,
websocket = $5,
force_http1 = $6,
server_timeout_seconds = $7,
active = $8,
updated_at = NOW()
WHERE id = $9
RETURNING id, name, scheme, health_check_path, lb_algorithm, websocket, force_http1,
server_timeout_seconds, active, created_at, updated_at`,
b.Name, b.Scheme, b.HealthCheckPath, b.LBAlgorithm, b.WebSocket, b.ForceHTTP1,
b.ServerTimeoutSeconds, b.Active, id)
out, err := scanBackend(row)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrNotFound
}
return nil, err
}
return out, nil
}
func (r *Repo) Delete(ctx context.Context, id int64) error {
var n int
if err := r.Pool.QueryRow(ctx,
`SELECT COUNT(*) FROM routing_rules WHERE backend_id = $1`, id,
).Scan(&n); err != nil {
return err
}
if n > 0 {
return ErrInUse
}
tag, err := r.Pool.Exec(ctx, `DELETE FROM backends WHERE id = $1`, id)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return ErrNotFound
}
return nil
}
func scanBackend(row interface{ Scan(...any) error }) (*models.Backend, error) {
var b models.Backend
if err := row.Scan(
&b.ID, &b.Name, &b.Scheme,
&b.HealthCheckPath, &b.LBAlgorithm, &b.WebSocket, &b.ForceHTTP1,
&b.ServerTimeoutSeconds, &b.Active,
&b.CreatedAt, &b.UpdatedAt,
); err != nil {
return nil, err
}
return &b, nil
}