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:
@@ -83,8 +83,15 @@ frontend internal_stats
|
||||
backend api_backend
|
||||
server api1 127.0.0.1:9443 check
|
||||
|
||||
{{- range .Backends}}
|
||||
{{- range $b := .Backends}}
|
||||
|
||||
backend eg_backend_{{.ID}}
|
||||
server {{.Name | safeID}} {{.Address}}:{{.Port}}{{if eq .Scheme "https"}} ssl verify none alpn h2,http/1.1{{end}}{{if .HealthCheckPath}} check inter 5s{{end}}
|
||||
backend eg_backend_{{$b.ID}}
|
||||
balance {{$b.LBAlgorithm}}
|
||||
{{- if $b.HealthCheckPath}}
|
||||
option httpchk
|
||||
http-check send meth GET uri {{$b.HealthCheckPath}}
|
||||
{{- end}}
|
||||
{{- range $s := $b.Servers}}
|
||||
server {{$s.Name | safeID}} {{$s.Address}}:{{$s.Port}}{{if eq $b.Scheme "https"}} ssl verify none alpn h2,http/1.1{{end}}{{if $b.HealthCheckPath}} check inter 5s{{end}} weight {{$s.Weight}}{{if $s.Backup}} backup{{end}}
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -17,6 +17,21 @@ func renderView(t *testing.T, v View) string {
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func mkBackend(id int64, name string, hcp *string) models.Backend {
|
||||
return models.Backend{
|
||||
ID: id, Name: name, Scheme: "http",
|
||||
LBAlgorithm: "roundrobin", Active: true,
|
||||
HealthCheckPath: hcp,
|
||||
}
|
||||
}
|
||||
|
||||
func mkServer(backendID int64, name, addr string, port int) models.BackendServer {
|
||||
return models.BackendServer{
|
||||
BackendID: backendID, Name: name, Address: addr, Port: port,
|
||||
Weight: 100, Active: true,
|
||||
}
|
||||
}
|
||||
|
||||
func TestRender_BaselineHasFrontendsAndApiBackend(t *testing.T) {
|
||||
out := renderView(t, View{})
|
||||
for _, w := range []string{
|
||||
@@ -37,9 +52,13 @@ func TestRender_BaselineHasFrontendsAndApiBackend(t *testing.T) {
|
||||
|
||||
func TestRender_DomainRoutesEmitUseBackend(t *testing.T) {
|
||||
v := View{
|
||||
Backends: []models.Backend{
|
||||
{ID: 1, Name: "app", Address: "10.0.0.10", Port: 8080, Active: true},
|
||||
{ID: 2, Name: "api", Address: "10.0.0.20", Port: 9000, Active: true},
|
||||
Backends: []BackendView{
|
||||
{Backend: mkBackend(1, "app", nil), Servers: []models.BackendServer{
|
||||
mkServer(1, "app", "10.0.0.10", 8080),
|
||||
}},
|
||||
{Backend: mkBackend(2, "api", nil), Servers: []models.BackendServer{
|
||||
mkServer(2, "api", "10.0.0.20", 9000),
|
||||
}},
|
||||
},
|
||||
Domains: []DomainView{{
|
||||
Domain: models.Domain{ID: 1, Name: "example.com", Active: true},
|
||||
@@ -55,6 +74,7 @@ func TestRender_DomainRoutesEmitUseBackend(t *testing.T) {
|
||||
"server app 10.0.0.10:8080",
|
||||
"backend eg_backend_2",
|
||||
"server api 10.0.0.20:9000",
|
||||
"balance roundrobin",
|
||||
"use_backend eg_backend_1 if { hdr(host) -i example.com } { path_beg / }",
|
||||
"use_backend eg_backend_2 if { hdr(host) -i example.com } { path_beg /api }",
|
||||
} {
|
||||
@@ -67,12 +87,46 @@ func TestRender_DomainRoutesEmitUseBackend(t *testing.T) {
|
||||
func TestRender_HealthCheckPathAddsCheckInter(t *testing.T) {
|
||||
hcp := "/health"
|
||||
v := View{
|
||||
Backends: []models.Backend{
|
||||
{ID: 1, Name: "app", Address: "10.0.0.10", Port: 8080, Active: true, HealthCheckPath: &hcp},
|
||||
Backends: []BackendView{
|
||||
{Backend: mkBackend(1, "app", &hcp), Servers: []models.BackendServer{
|
||||
mkServer(1, "app", "10.0.0.10", 8080),
|
||||
}},
|
||||
},
|
||||
}
|
||||
out := renderView(t, v)
|
||||
if !strings.Contains(out, "server app 10.0.0.10:8080 check inter 5s") {
|
||||
t.Errorf("expected `check inter 5s` for backend with health_check_path:\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, "option httpchk") {
|
||||
t.Errorf("expected `option httpchk` when health_check_path set:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRender_MultiServerPool(t *testing.T) {
|
||||
v := View{
|
||||
Backends: []BackendView{
|
||||
{
|
||||
Backend: models.Backend{ID: 1, Name: "vmm", Scheme: "http", LBAlgorithm: "leastconn", Active: true},
|
||||
Servers: []models.BackendServer{
|
||||
{BackendID: 1, Name: "vmm-1", Address: "10.0.0.11", Port: 8080, Weight: 100, Active: true},
|
||||
{BackendID: 1, Name: "vmm-2", Address: "10.0.0.12", Port: 8080, Weight: 100, Active: true},
|
||||
{BackendID: 1, Name: "vmm-3", Address: "10.0.0.13", Port: 8080, Weight: 50, Backup: true, Active: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
out := renderView(t, v)
|
||||
for _, w := range []string{
|
||||
"backend eg_backend_1",
|
||||
"balance leastconn",
|
||||
"server vmm-1 10.0.0.11:8080",
|
||||
"server vmm-2 10.0.0.12:8080",
|
||||
"server vmm-3 10.0.0.13:8080",
|
||||
"weight 50",
|
||||
" backup",
|
||||
} {
|
||||
if !strings.Contains(out, w) {
|
||||
t.Errorf("missing %q in multi-server output:\n%s", w, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user