// Package haproxy renders /etc/edgeguard/haproxy/haproxy.cfg from // the relational state in PG. v1 is the full ingress: HAProxy // terminates TLS on :443, redirects :80 → :443 (except ACME), routes // by Host header to user backends, and falls through to edgeguard-api // for management UI + ACME webroot. package haproxy import ( "bytes" "context" _ "embed" "fmt" "path/filepath" "text/template" "github.com/jackc/pgx/v5/pgxpool" "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/domains" "git.netcell-it.de/projekte/edgeguard-native/internal/services/routingrules" ) //go:embed haproxy.cfg.tpl var cfgTpl string var tpl = template.Must(template.New("haproxy").Parse(cfgTpl)) type Generator struct { Pool *pgxpool.Pool DomainsRepo *domains.Repo BackendsRepo *backends.Repo RoutingRepo *routingrules.Repo OutputPath string SkipReload bool } func New(pool *pgxpool.Pool) *Generator { return &Generator{ Pool: pool, DomainsRepo: domains.New(pool), BackendsRepo: backends.New(pool), RoutingRepo: routingrules.New(pool), } } func (g *Generator) Name() string { return "haproxy" } func (g *Generator) Render(ctx context.Context) error { view, err := g.loadView(ctx) if err != nil { return fmt.Errorf("haproxy: load state: %w", err) } var buf bytes.Buffer if err := tpl.Execute(&buf, view); err != nil { return fmt.Errorf("haproxy: render template: %w", err) } out := g.OutputPath if out == "" { out = filepath.Join(configgen.EtcEdgeguard, "haproxy", "haproxy.cfg") } if err := configgen.AtomicWrite(out, buf.Bytes(), 0o644); err != nil { return fmt.Errorf("haproxy: write: %w", err) } if g.SkipReload { return nil } if err := configgen.ReloadService("haproxy"); err != nil { return fmt.Errorf("haproxy: reload: %w", err) } return nil } // View is what the template consumes. Routes per domain are pre- // joined here so the template can stay declarative. type View struct { Domains []DomainView Backends []models.Backend } type DomainView struct { models.Domain Routes []RouteView } type RouteView struct { PathPrefix string BackendID int64 } func (g *Generator) loadView(ctx context.Context) (*View, error) { doms, err := g.DomainsRepo.List(ctx) if err != nil { return nil, fmt.Errorf("list domains: %w", err) } bes, err := g.BackendsRepo.List(ctx) if err != nil { return nil, fmt.Errorf("list backends: %w", err) } rules, err := g.RoutingRepo.List(ctx) if err != nil { return nil, fmt.Errorf("list routing rules: %w", err) } rulesByDomain := map[int64][]RouteView{} for _, r := range rules { if !r.Active { continue } rulesByDomain[r.DomainID] = append(rulesByDomain[r.DomainID], RouteView{ PathPrefix: r.PathPrefix, BackendID: r.BackendID, }) } activeBackends := make([]models.Backend, 0, len(bes)) for _, b := range bes { if b.Active { activeBackends = append(activeBackends, b) } } domViews := make([]DomainView, 0, len(doms)) for _, d := range doms { if !d.Active { continue } domViews = append(domViews, DomainView{ Domain: d, Routes: rulesByDomain[d.ID], }) } return &View{Domains: domViews, Backends: activeBackends}, nil }