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:
2026-07-09 22:26:56 +00:00
parent 2e6fce54e3
commit c18217944f
6 changed files with 222 additions and 54 deletions

View File

@@ -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")
}
}
}