Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0567505a51 | |||
| b0e3e31a6b |
+11
-1
@@ -241,7 +241,17 @@ func hooks(in RenderInput) []hookLib {
|
|||||||
"max-response-delay": firstNonZero(ha.MaxResponseDelay, 60000),
|
"max-response-delay": firstNonZero(ha.MaxResponseDelay, 60000),
|
||||||
"max-ack-delay": firstNonZero(ha.MaxAckDelay, 5000),
|
"max-ack-delay": firstNonZero(ha.MaxAckDelay, 5000),
|
||||||
"max-unacked-clients": firstNonZero(ha.MaxUnackedClients, 5),
|
"max-unacked-clients": firstNonZero(ha.MaxUnackedClients, 5),
|
||||||
"peers": peers,
|
// With core multi-threading enabled (Kea 2.6 default) the HA hook would
|
||||||
|
// open a dedicated HTTP listener bound to this server's peer url address —
|
||||||
|
// here a per-pod ClusterIP (virtual, kube-proxy DNAT) that is not
|
||||||
|
// assignable on the pod, so the bind fails. Disable it so inbound HA
|
||||||
|
// traffic flows via kea-ctrl-agent, which binds 0.0.0.0:CtrlAgentPort;
|
||||||
|
// peers stay reachable at their ClusterIP:CtrlAgentPort via the per-pod Service.
|
||||||
|
"multi-threading": map[string]any{
|
||||||
|
"enable-multi-threading": true,
|
||||||
|
"http-dedicated-listener": false,
|
||||||
|
},
|
||||||
|
"peers": peers,
|
||||||
}
|
}
|
||||||
libs = append(libs, hookLib{
|
libs = append(libs, hookLib{
|
||||||
Library: HALibrary,
|
Library: HALibrary,
|
||||||
|
|||||||
@@ -114,6 +114,53 @@ func TestRenderDHCP4ReferenceSemantics(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestHADedicatedListenerDisabled guards the HA listener/bind fix: with Kea 2.6
|
||||||
|
// core multi-threading on by default, the HA hook would otherwise open a
|
||||||
|
// dedicated HTTP listener bound to this server's peer url — a per-pod ClusterIP
|
||||||
|
// that is virtual (kube-proxy DNAT) and unassignable on the pod, failing with
|
||||||
|
// "Cannot assign requested address". The rendered config must disable the
|
||||||
|
// dedicated listener so inbound HA traffic is served by kea-ctrl-agent
|
||||||
|
// (0.0.0.0:8000) while peers stay reachable at their ClusterIP:8000.
|
||||||
|
func TestHADedicatedListenerDisabled(t *testing.T) {
|
||||||
|
out, err := RenderDHCP4(referenceInput())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("render: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(out, `"http-dedicated-listener": false`) {
|
||||||
|
t.Errorf("HA hook must disable the dedicated HTTP listener (it binds this server's ClusterIP peer url, unassignable on the pod); got:\n%s", out)
|
||||||
|
}
|
||||||
|
if !strings.Contains(out, `"enable-multi-threading": true`) {
|
||||||
|
t.Errorf("HA multi-threading must stay enabled (CA-mediated HA traffic); got:\n%s", out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The CA-mediated route only works because the dedicated listener stays off
|
||||||
|
// on the same port the ctrl-agent binds: parse out the HA hook and assert it.
|
||||||
|
var root dhcp4Root
|
||||||
|
if err := json.Unmarshal([]byte(out), &root); err != nil {
|
||||||
|
t.Fatalf("unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
var haParams map[string]any
|
||||||
|
for _, h := range root.Dhcp4.HooksLibraries {
|
||||||
|
if strings.Contains(h.Library, "libdhcp_ha.so") {
|
||||||
|
haParams = h.Parameters
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if haParams == nil {
|
||||||
|
t.Fatal("HA hook library not present in rendered config")
|
||||||
|
}
|
||||||
|
rels, ok := haParams["high-availability"].([]any)
|
||||||
|
if !ok || len(rels) != 1 {
|
||||||
|
t.Fatalf("high-availability block malformed: %#v", haParams["high-availability"])
|
||||||
|
}
|
||||||
|
mt, ok := rels[0].(map[string]any)["multi-threading"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("HA relationship missing multi-threading block: %#v", rels[0])
|
||||||
|
}
|
||||||
|
if mt["http-dedicated-listener"] != false {
|
||||||
|
t.Errorf("http-dedicated-listener must be false, got %#v", mt["http-dedicated-listener"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestSubnetWithoutPoolIsDeclared verifies the pool-less subnet still appears
|
// TestSubnetWithoutPoolIsDeclared verifies the pool-less subnet still appears
|
||||||
// (Kea must know the subnet to service relayed requests) but carries no pools.
|
// (Kea must know the subnet to service relayed requests) but carries no pools.
|
||||||
func TestSubnetWithoutPoolIsDeclared(t *testing.T) {
|
func TestSubnetWithoutPoolIsDeclared(t *testing.T) {
|
||||||
@@ -231,6 +278,12 @@ func TestRenderCtrlAgent(t *testing.T) {
|
|||||||
t.Errorf("ctrl-agent config missing %q", m)
|
t.Errorf("ctrl-agent config missing %q", m)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// The HA hook runs with the dedicated listener disabled, so inbound HA traffic
|
||||||
|
// is served by the ctrl-agent; it must bind a pod-local address (0.0.0.0), not
|
||||||
|
// a ClusterIP, so peers reaching the per-pod Service's ClusterIP:8000 land here.
|
||||||
|
if !strings.Contains(out, `"http-host": "0.0.0.0"`) {
|
||||||
|
t.Errorf("ctrl-agent must bind 0.0.0.0 (pod-local) for CA-mediated HA, got: %s", out)
|
||||||
|
}
|
||||||
// Kea 2.6+ only accepts unix socket paths under /var/run/kea (exact string).
|
// Kea 2.6+ only accepts unix socket paths under /var/run/kea (exact string).
|
||||||
if !strings.Contains(out, `"socket-name": "/var/run/kea/`) {
|
if !strings.Contains(out, `"socket-name": "/var/run/kea/`) {
|
||||||
t.Errorf("ctrl-agent socket-name must be under /var/run/kea, got: %s", out)
|
t.Errorf("ctrl-agent socket-name must be under /var/run/kea, got: %s", out)
|
||||||
|
|||||||
Reference in New Issue
Block a user