feat(backends): Pool-Modell — Backend = Pool, N Server pro Backend

Migration 0016: backend_servers (id, backend_id, name, address, port,
weight, backup, active) + backends.lb_algorithm. Daten-Migration kopiert
bestehende backends.address/port als ersten Server, dann DROP COLUMN.

HAProxy-Renderer: rendert pro Backend einen Block mit `balance <algo>`
+ N `server`-Zeilen (weight, backup-Flag, optional check inter 5s).
LB-Algorithmen: roundrobin / leastconn / source.

REST: /backends/:id/servers (GET/POST), /backend-servers/:id (PUT/DELETE).
Re-rendert HAProxy nach jeder Server-Mutation.

UI: address/port aus Backend-Form raus, lb_algorithm-Select rein. Server
verwaltet ein expandable Sub-Panel pro Backend-Row (Tabelle + Add/Edit/
Delete-Modal). Domain-Attachment-Multi-Select bleibt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Debian
2026-05-11 20:55:47 +02:00
parent 05850934fb
commit 8aac24b566
16 changed files with 784 additions and 85 deletions

View File

@@ -19,6 +19,7 @@ import (
"git.netcell-it.de/projekte/edgeguard-native/internal/configgen"
"git.netcell-it.de/projekte/edgeguard-native/internal/models"
"git.netcell-it.de/projekte/edgeguard-native/internal/services/backends"
"git.netcell-it.de/projekte/edgeguard-native/internal/services/backendservers"
"git.netcell-it.de/projekte/edgeguard-native/internal/services/domains"
"git.netcell-it.de/projekte/edgeguard-native/internal/services/routingrules"
)
@@ -55,10 +56,11 @@ var tpl = template.Must(template.New("haproxy").Funcs(template.FuncMap{
}).Parse(cfgTpl))
type Generator struct {
Pool *pgxpool.Pool
DomainsRepo *domains.Repo
BackendsRepo *backends.Repo
RoutingRepo *routingrules.Repo
Pool *pgxpool.Pool
DomainsRepo *domains.Repo
BackendsRepo *backends.Repo
ServersRepo *backendservers.Repo
RoutingRepo *routingrules.Repo
OutputPath string
SkipReload bool
@@ -69,6 +71,7 @@ func New(pool *pgxpool.Pool) *Generator {
Pool: pool,
DomainsRepo: domains.New(pool),
BackendsRepo: backends.New(pool),
ServersRepo: backendservers.New(pool),
RoutingRepo: routingrules.New(pool),
}
}
@@ -101,10 +104,12 @@ func (g *Generator) Render(ctx context.Context) error {
}
// View is what the template consumes. Routes per domain are pre-
// joined here so the template can stay declarative.
// joined here so the template can stay declarative; Servers leben pro
// BackendView, damit das Template einen `backend …`-Block mit den N
// Server-Zeilen rendern kann.
type View struct {
Domains []DomainView
Backends []models.Backend
Backends []BackendView
}
type DomainView struct {
@@ -117,6 +122,11 @@ type RouteView struct {
BackendID int64
}
type BackendView struct {
models.Backend
Servers []models.BackendServer
}
func (g *Generator) loadView(ctx context.Context) (*View, error) {
doms, err := g.DomainsRepo.List(ctx)
if err != nil {
@@ -126,6 +136,10 @@ func (g *Generator) loadView(ctx context.Context) (*View, error) {
if err != nil {
return nil, fmt.Errorf("list backends: %w", err)
}
srvs, err := g.ServersRepo.ListAll(ctx)
if err != nil {
return nil, fmt.Errorf("list backend servers: %w", err)
}
rules, err := g.RoutingRepo.List(ctx)
if err != nil {
return nil, fmt.Errorf("list routing rules: %w", err)
@@ -142,11 +156,23 @@ func (g *Generator) loadView(ctx context.Context) (*View, error) {
})
}
activeBackends := make([]models.Backend, 0, len(bes))
for _, b := range bes {
if b.Active {
activeBackends = append(activeBackends, b)
srvByBackend := map[int64][]models.BackendServer{}
for _, s := range srvs {
if !s.Active {
continue
}
srvByBackend[s.BackendID] = append(srvByBackend[s.BackendID], s)
}
activeBackends := make([]BackendView, 0, len(bes))
for _, b := range bes {
if !b.Active {
continue
}
activeBackends = append(activeBackends, BackendView{
Backend: b,
Servers: srvByBackend[b.ID],
})
}
domViews := make([]DomainView, 0, len(doms))