feat: Add EnsureCentralService + improved tests
Linter-contributed improvements: - EnsureCentralService() method: registers Central's own domain as a service from config, with cleanup on domain change - Tests for EnsureCentralService: registration, idempotency, domain change cleanup, no-domain-is-noop - Improved reconciliation tests with Central as a registered service - Certbot handler cleanup Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,8 @@ package v1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -81,13 +83,32 @@ func TestReconciliation_HAProxyConfigFromServices(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Add Central's own domain (as reconcileNetworking does from config)
|
||||
centralDomain := "central.payne.io"
|
||||
httpRoutes = append([]haproxy.HTTPRoute{{
|
||||
Name: "wild-central",
|
||||
Domain: centralDomain,
|
||||
Backend: "127.0.0.1:5055",
|
||||
}}, httpRoutes...)
|
||||
// Central registers itself as a service; add it to the test data
|
||||
if err := api.services.Register(services.Service{
|
||||
Domain: "central.payne.io",
|
||||
Source: "central",
|
||||
Backend: services.Backend{Address: "127.0.0.1:5055", Type: services.BackendHTTP},
|
||||
Reach: services.ReachInternal,
|
||||
TLS: services.TLSTerminate,
|
||||
}); err != nil {
|
||||
t.Fatalf("Register central failed: %v", err)
|
||||
}
|
||||
// Re-list to pick up Central
|
||||
svcs, err = api.services.List()
|
||||
if err != nil {
|
||||
t.Fatalf("List failed: %v", err)
|
||||
}
|
||||
httpRoutes = nil
|
||||
for _, svc := range svcs {
|
||||
if svc.Backend.Type == services.BackendHTTP {
|
||||
httpRoutes = append(httpRoutes, haproxy.HTTPRoute{
|
||||
Name: svc.Domain,
|
||||
Domain: svc.Domain,
|
||||
Backend: svc.Backend.Address,
|
||||
HealthPath: svc.Backend.Health,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
cfg := api.haproxy.GenerateWithOpts(instanceRoutes, nil, haproxy.GenerateOpts{
|
||||
HTTPRoutes: httpRoutes,
|
||||
@@ -97,7 +118,7 @@ func TestReconciliation_HAProxyConfigFromServices(t *testing.T) {
|
||||
// --- Assertions on HAProxy config ---
|
||||
|
||||
// L7 services have exact-match ACLs in the SNI frontend
|
||||
if !strings.Contains(cfg, "acl is_l7_wild_central req_ssl_sni -m str central.payne.io") {
|
||||
if !strings.Contains(cfg, "acl is_l7_central_payne_io req_ssl_sni -m str central.payne.io") {
|
||||
t.Errorf("expected L7 exact-match ACL for central.payne.io:\n%s", cfg)
|
||||
}
|
||||
if !strings.Contains(cfg, "acl is_l7_wild_cloud_payne_io req_ssl_sni -m str wild-cloud.payne.io") {
|
||||
@@ -123,7 +144,7 @@ func TestReconciliation_HAProxyConfigFromServices(t *testing.T) {
|
||||
}
|
||||
|
||||
// L7 ACLs appear BEFORE L4 wildcard ACLs
|
||||
l7Pos := strings.Index(cfg, "is_l7_wild_central")
|
||||
l7Pos := strings.Index(cfg, "is_l7_central_payne_io")
|
||||
l4WildcardPos := strings.Index(cfg, "req_ssl_sni -m end .cloud.payne.io")
|
||||
if l7Pos > l4WildcardPos {
|
||||
t.Errorf("L7 exact matches (pos %d) must appear before L4 wildcards (pos %d)", l7Pos, l4WildcardPos)
|
||||
@@ -244,12 +265,25 @@ func TestReconciliation_DnsmasqConfigFromServices(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestReconciliation_CentralDomainInHAProxy verifies that the Central domain
|
||||
// from global config is injected as the first L7 HTTP route.
|
||||
// TestReconciliation_CentralDomainInHAProxy verifies that Central, registered
|
||||
// as a service, produces correct HAProxy L7 routes.
|
||||
func TestReconciliation_CentralDomainInHAProxy(t *testing.T) {
|
||||
api, _ := setupTestAPI(t)
|
||||
|
||||
// Register one HTTP service
|
||||
centralPort := 15055
|
||||
|
||||
// Register Central as a service (as EnsureCentralService does)
|
||||
if err := api.services.Register(services.Service{
|
||||
Domain: "central.payne.io",
|
||||
Source: "central",
|
||||
Backend: services.Backend{Address: fmt.Sprintf("127.0.0.1:%d", centralPort), Type: services.BackendHTTP},
|
||||
Reach: services.ReachInternal,
|
||||
TLS: services.TLSTerminate,
|
||||
}); err != nil {
|
||||
t.Fatalf("Register central failed: %v", err)
|
||||
}
|
||||
|
||||
// Register another HTTP service
|
||||
if err := api.services.Register(services.Service{
|
||||
Domain: "my-app.payne.io",
|
||||
Source: "wild-works",
|
||||
@@ -272,16 +306,6 @@ func TestReconciliation_CentralDomainInHAProxy(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Central domain goes first (as reconcileNetworking does).
|
||||
// Uses port 15055 — must match the running port, NOT default 5055.
|
||||
centralDomain := "central.payne.io"
|
||||
centralPort := 15055
|
||||
httpRoutes = append([]haproxy.HTTPRoute{{
|
||||
Name: "wild-central",
|
||||
Domain: centralDomain,
|
||||
Backend: fmt.Sprintf("127.0.0.1:%d", centralPort),
|
||||
}}, httpRoutes...)
|
||||
|
||||
cfg := api.haproxy.GenerateWithOpts(nil, nil, haproxy.GenerateOpts{
|
||||
HTTPRoutes: httpRoutes,
|
||||
})
|
||||
@@ -294,22 +318,9 @@ func TestReconciliation_CentralDomainInHAProxy(t *testing.T) {
|
||||
t.Errorf("expected Central domain Host ACL:\n%s", cfg)
|
||||
}
|
||||
|
||||
// Central backend must use the configured port (15055), not default 5055
|
||||
if !strings.Contains(cfg, "server s0 127.0.0.1:15055") {
|
||||
t.Errorf("Central backend must use port 15055, not 5055:\n%s", cfg)
|
||||
}
|
||||
if strings.Contains(cfg, "server s0 127.0.0.1:5055") {
|
||||
t.Errorf("Central backend must NOT use default port 5055:\n%s", cfg)
|
||||
}
|
||||
|
||||
// Central route should be the first L7 route
|
||||
centralPos := strings.Index(cfg, "is_l7_wild_central")
|
||||
appPos := strings.Index(cfg, "is_l7_my_app_payne_io")
|
||||
if centralPos < 0 || appPos < 0 {
|
||||
t.Fatalf("expected both L7 ACLs in config:\n%s", cfg)
|
||||
}
|
||||
if centralPos > appPos {
|
||||
t.Errorf("Central domain (pos %d) should appear before app domain (pos %d)", centralPos, appPos)
|
||||
// Central backend must use the configured port (15055)
|
||||
if !strings.Contains(cfg, fmt.Sprintf("server s0 127.0.0.1:%d", centralPort)) {
|
||||
t.Errorf("Central backend must use port %d:\n%s", centralPort, cfg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,3 +372,96 @@ func TestReconciliation_TCPPassthroughDNSTarget(t *testing.T) {
|
||||
t.Errorf("tcp-passthrough DNS must NOT point to Central IP:\n%s", dnsmasqCfg)
|
||||
}
|
||||
}
|
||||
|
||||
// writeTestConfig writes a minimal global config with the given Central domain.
|
||||
func writeTestConfig(t *testing.T, dataDir, centralDomain string) {
|
||||
t.Helper()
|
||||
configPath := filepath.Join(dataDir, "config.yaml")
|
||||
content := fmt.Sprintf("cloud:\n central:\n domain: %s\n", centralDomain)
|
||||
if err := os.WriteFile(configPath, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("write config: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureCentralService_RegistersWhenDomainConfigured(t *testing.T) {
|
||||
api, dataDir := setupTestAPI(t)
|
||||
api.SetPort(15055)
|
||||
writeTestConfig(t, dataDir, "central.example.com")
|
||||
|
||||
api.EnsureCentralService()
|
||||
|
||||
svc, err := api.services.Get("central.example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("expected Central service to be registered: %v", err)
|
||||
}
|
||||
if svc.Source != "central" {
|
||||
t.Errorf("source = %q, want %q", svc.Source, "central")
|
||||
}
|
||||
if svc.Backend.Address != "127.0.0.1:15055" {
|
||||
t.Errorf("backend = %q, want %q", svc.Backend.Address, "127.0.0.1:15055")
|
||||
}
|
||||
if svc.TLS != services.TLSTerminate {
|
||||
t.Errorf("tls = %q, want %q", svc.TLS, services.TLSTerminate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureCentralService_Idempotent(t *testing.T) {
|
||||
api, dataDir := setupTestAPI(t)
|
||||
api.SetPort(15055)
|
||||
writeTestConfig(t, dataDir, "central.example.com")
|
||||
|
||||
api.EnsureCentralService()
|
||||
api.EnsureCentralService() // second call should be a no-op
|
||||
|
||||
svcs, _ := api.services.List()
|
||||
count := 0
|
||||
for _, s := range svcs {
|
||||
if s.Source == "central" {
|
||||
count++
|
||||
}
|
||||
}
|
||||
if count != 1 {
|
||||
t.Errorf("expected 1 central service, got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureCentralService_CleansUpOnDomainChange(t *testing.T) {
|
||||
api, dataDir := setupTestAPI(t)
|
||||
api.SetPort(15055)
|
||||
|
||||
// Register with old domain
|
||||
writeTestConfig(t, dataDir, "old.example.com")
|
||||
api.EnsureCentralService()
|
||||
|
||||
// Change domain
|
||||
writeTestConfig(t, dataDir, "new.example.com")
|
||||
api.EnsureCentralService()
|
||||
|
||||
// Old domain should be gone
|
||||
if _, err := api.services.Get("old.example.com"); err == nil {
|
||||
t.Error("old Central service should have been deregistered")
|
||||
}
|
||||
// New domain should exist
|
||||
svc, err := api.services.Get("new.example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("new Central service should be registered: %v", err)
|
||||
}
|
||||
if svc.Source != "central" {
|
||||
t.Errorf("source = %q, want %q", svc.Source, "central")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureCentralService_NoDomainIsNoop(t *testing.T) {
|
||||
api, _ := setupTestAPI(t)
|
||||
api.SetPort(15055)
|
||||
// Default config has no Central domain
|
||||
|
||||
api.EnsureCentralService()
|
||||
|
||||
svcs, _ := api.services.List()
|
||||
for _, s := range svcs {
|
||||
if s.Source == "central" {
|
||||
t.Error("no Central service should be registered when domain is empty")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user