feat: Extract Wild Central as standalone Go service

Extract the Central networking functionality from wild-cloud/api into
a standalone service. Wild Central manages DNS (dnsmasq), gateway
(HAProxy), firewall (nftables), VPN (WireGuard), TLS (certbot),
security (CrowdSec), and DDNS — all the network appliance concerns.

All Cloud-specific code (instances, clusters, nodes, apps, backups,
operations, kubectl/talosctl tooling) has been removed. The API struct
and route registration contain only Central endpoints. Tests updated
to match the new API signature.

Builds and all tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-08 23:31:16 +00:00
commit beb643f76f
52 changed files with 12814 additions and 0 deletions

View File

@@ -0,0 +1,130 @@
package v1
import (
"encoding/json"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
)
func setupTestNftables(t *testing.T) (*API, string) {
tmpDir := t.TempDir()
t.Setenv("WILD_CENTRAL_HAPROXY_CONFIG_PATH", filepath.Join(tmpDir, "haproxy.cfg"))
t.Setenv("WILD_CENTRAL_NFTABLES_RULES_PATH", filepath.Join(tmpDir, "wild-central.nft"))
api, err := NewAPI(tmpDir, "", nil)
if err != nil {
t.Fatalf("failed to create test API: %v", err)
}
return api, tmpDir
}
// TestNftablesStatus_ReturnsOK verifies the status endpoint returns 200.
// GetStatus() runs `nft list table inet wild-cloud` and silently returns an
// empty string if nft is not installed or the table doesn't exist, so this
// endpoint always returns 200.
func TestNftablesStatus_ReturnsOK(t *testing.T) {
api, tmpDir := setupTestNftables(t)
req := httptest.NewRequest("GET", "/api/v1/nftables/status", nil)
w := httptest.NewRecorder()
api.NftablesStatus(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
}
var resp map[string]any
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("invalid JSON: %v", err)
}
if _, ok := resp["rulesFile"]; !ok {
t.Error("expected 'rulesFile' field in response")
}
if _, ok := resp["rules"]; !ok {
t.Error("expected 'rules' field in response")
}
rulesFile, _ := resp["rulesFile"].(string)
expected := filepath.Join(tmpDir, "wild-central.nft")
if rulesFile != expected {
t.Errorf("expected rulesFile=%q, got %q", expected, rulesFile)
}
}
// TestNftablesGetInterfaces_ReturnsOK verifies the interfaces endpoint returns 200
// with a valid interfaces list. Uses the real net.Interfaces() so we just check
// structure — the test host will always have at least one interface (lo is excluded,
// but any other up interface counts; if only loopback exists the list can be empty).
func TestNftablesGetInterfaces_ReturnsOK(t *testing.T) {
api, _ := setupTestNftables(t)
req := httptest.NewRequest("GET", "/api/v1/nftables/interfaces", nil)
w := httptest.NewRecorder()
api.NftablesGetInterfaces(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
}
var resp map[string]any
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("invalid JSON: %v", err)
}
ifaces, ok := resp["interfaces"]
if !ok {
t.Fatal("expected 'interfaces' field in response")
}
// Each entry must have a name and addresses array
for _, entry := range ifaces.([]any) {
iface := entry.(map[string]any)
name, hasName := iface["name"].(string)
if !hasName || name == "" {
t.Errorf("interface entry missing non-empty name: %v", iface)
}
if _, hasAddrs := iface["addresses"]; !hasAddrs {
t.Errorf("interface %q missing addresses field", name)
}
// Loopback must not appear
if name == "lo" {
t.Errorf("loopback interface 'lo' should be excluded")
}
}
}
// TestNftablesApply_ServiceUnavailable verifies the handler returns 500 when
// the nftables reload service is not available (expected in test environments).
func TestNftablesApply_ServiceUnavailable(t *testing.T) {
api, _ := setupTestNftables(t)
req := httptest.NewRequest("POST", "/api/v1/nftables/apply", nil)
w := httptest.NewRecorder()
api.NftablesApply(w, req)
// ApplyRules calls `systemctl start wild-cloud-nftables-reload.service`
// which requires polkit/root — always fails in test environments.
if w.Code != http.StatusInternalServerError {
// If it happened to succeed (running on the actual Wild Central device
// with the right polkit rules), that's also valid.
if w.Code != http.StatusOK {
t.Errorf("unexpected status %d: %s", w.Code, w.Body.String())
}
return
}
var resp map[string]any
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("invalid JSON: %v", err)
}
if resp["error"] == nil {
t.Error("expected 'error' field in 500 response")
}
}