mirror of
https://github.com/coredns/coredns.git
synced 2026-08-27 23:27:05 -04:00
plugin/kubernetes: Add support for topology-aware headless services via "az-pinned" subdomains (#8388)
This commit is contained in:
263
plugin/kubernetes/zonal_test.go
Normal file
263
plugin/kubernetes/zonal_test.go
Normal file
@@ -0,0 +1,263 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/coredns/coredns/plugin/kubernetes/object"
|
||||
"github.com/coredns/coredns/plugin/pkg/dnstest"
|
||||
"github.com/coredns/coredns/plugin/test"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
api "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
type APIConnZonalTest struct{}
|
||||
|
||||
func (APIConnZonalTest) HasSynced() bool { return true }
|
||||
func (APIConnZonalTest) Run() {}
|
||||
func (APIConnZonalTest) Stop() error { return nil }
|
||||
func (APIConnZonalTest) PodIndex(string) []*object.Pod { return nil }
|
||||
func (APIConnZonalTest) SvcIndexReverse(string) []*object.Service { return nil }
|
||||
func (APIConnZonalTest) SvcExtIndexReverse(string) []*object.Service { return nil }
|
||||
func (APIConnZonalTest) ServiceImportList() []*object.ServiceImport { return nil }
|
||||
func (APIConnZonalTest) SvcImportIndex(string) []*object.ServiceImport { return nil }
|
||||
func (APIConnZonalTest) EpIndexReverse(string) []*object.Endpoints { return nil }
|
||||
func (APIConnZonalTest) McEpIndex(string) []*object.MultiClusterEndpoints { return nil }
|
||||
func (APIConnZonalTest) Modified(ModifiedMode) int64 { return int64(1499347823) }
|
||||
|
||||
func (a APIConnZonalTest) ServiceList() []*object.Service {
|
||||
return []*object.Service{
|
||||
{
|
||||
Name: "hdls",
|
||||
Namespace: "testns",
|
||||
ClusterIPs: []string{api.ClusterIPNone},
|
||||
},
|
||||
{
|
||||
Name: "clstr",
|
||||
Namespace: "testns",
|
||||
ClusterIPs: []string{"10.0.0.10"},
|
||||
Ports: []api.ServicePort{{Name: "http", Protocol: "tcp", Port: 80}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (a APIConnZonalTest) SvcIndex(idx string) []*object.Service {
|
||||
switch idx {
|
||||
case "hdls.testns":
|
||||
return a.ServiceList()[:1]
|
||||
case "clstr.testns":
|
||||
return a.ServiceList()[1:]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a APIConnZonalTest) EndpointsList() []*object.Endpoints {
|
||||
return []*object.Endpoints{
|
||||
{
|
||||
Subsets: []object.EndpointSubset{
|
||||
{
|
||||
Addresses: []object.EndpointAddress{
|
||||
{IP: "172.0.0.1"},
|
||||
{IP: "172.0.0.2"},
|
||||
{IP: "172.0.0.3"},
|
||||
},
|
||||
Ports: []object.EndpointPort{{Port: 80, Name: "http", Protocol: "tcp"}},
|
||||
},
|
||||
},
|
||||
Zones: map[string]string{
|
||||
"172.0.0.1": "us-west-2a",
|
||||
"172.0.0.2": "us-west-2b",
|
||||
"172.0.0.3": "us-west-2b",
|
||||
},
|
||||
Name: "hdls-slice",
|
||||
Namespace: "testns",
|
||||
Index: object.EndpointsKey("hdls", "testns"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (a APIConnZonalTest) EpIndex(idx string) []*object.Endpoints {
|
||||
if idx == "hdls.testns" {
|
||||
return a.EndpointsList()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (APIConnZonalTest) GetNodeByName(_ context.Context, _ string) (*api.Node, error) {
|
||||
return &api.Node{}, nil
|
||||
}
|
||||
|
||||
func (APIConnZonalTest) GetNamespaceByName(name string) (*object.Namespace, error) {
|
||||
return &object.Namespace{Name: name}, nil
|
||||
}
|
||||
|
||||
var zonalTestCases = []test.Case{
|
||||
{ // pin: endpoints narrowed to the requested zone
|
||||
Qname: "us-west-2a.pin._zone.hdls.testns.svc.cluster.local.", Qtype: dns.TypeA,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Answer: []dns.RR{
|
||||
test.A("us-west-2a.pin._zone.hdls.testns.svc.cluster.local. 5 IN A 172.0.0.1"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Qname: "us-west-2b.pin._zone.hdls.testns.svc.cluster.local.", Qtype: dns.TypeA,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Answer: []dns.RR{
|
||||
test.A("us-west-2b.pin._zone.hdls.testns.svc.cluster.local. 5 IN A 172.0.0.2"),
|
||||
test.A("us-west-2b.pin._zone.hdls.testns.svc.cluster.local. 5 IN A 172.0.0.3"),
|
||||
},
|
||||
},
|
||||
{ // pin: any zone label without matching endpoints — drained zones and
|
||||
// typos alike — is the same determinate empty answer
|
||||
Qname: "us-west-2c.pin._zone.hdls.testns.svc.cluster.local.", Qtype: dns.TypeA,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Ns: []dns.RR{
|
||||
test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 5"),
|
||||
},
|
||||
},
|
||||
{ // prefer: narrows exactly like pin when the zone is populated
|
||||
Qname: "us-west-2a.prefer._zone.hdls.testns.svc.cluster.local.", Qtype: dns.TypeA,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Answer: []dns.RR{
|
||||
test.A("us-west-2a.prefer._zone.hdls.testns.svc.cluster.local. 5 IN A 172.0.0.1"),
|
||||
},
|
||||
},
|
||||
{ // prefer: an empty zone falls back to every endpoint — the fallback
|
||||
// is chosen in the name, so it is not a silent widening of a pin
|
||||
Qname: "us-west-2c.prefer._zone.hdls.testns.svc.cluster.local.", Qtype: dns.TypeA,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Answer: []dns.RR{
|
||||
test.A("us-west-2c.prefer._zone.hdls.testns.svc.cluster.local. 5 IN A 172.0.0.1"),
|
||||
test.A("us-west-2c.prefer._zone.hdls.testns.svc.cluster.local. 5 IN A 172.0.0.2"),
|
||||
test.A("us-west-2c.prefer._zone.hdls.testns.svc.cluster.local. 5 IN A 172.0.0.3"),
|
||||
},
|
||||
},
|
||||
{ // a nonexistent service stays NXDOMAIN under any directive
|
||||
Qname: "us-west-2a.pin._zone.ghost.testns.svc.cluster.local.", Qtype: dns.TypeA,
|
||||
Rcode: dns.RcodeNameError,
|
||||
Ns: []dns.RR{
|
||||
test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 5"),
|
||||
},
|
||||
},
|
||||
{ // an unknown directive keeps the stock too-long NXDOMAIN
|
||||
Qname: "us-west-2a.florp._zone.hdls.testns.svc.cluster.local.", Qtype: dns.TypeA,
|
||||
Rcode: dns.RcodeNameError,
|
||||
Ns: []dns.RR{
|
||||
test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 5"),
|
||||
},
|
||||
},
|
||||
{ // zone-scoped names are defined for headless services only
|
||||
Qname: "us-west-2a.pin._zone.clstr.testns.svc.cluster.local.", Qtype: dns.TypeA,
|
||||
Rcode: dns.RcodeNameError,
|
||||
Ns: []dns.RR{
|
||||
test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 5"),
|
||||
},
|
||||
},
|
||||
{ // pin: exists-but-empty stays NODATA for non-address qtypes too (TXT
|
||||
// has its own lookup branch; NXDOMAIN would be negative-cached per name)
|
||||
Qname: "us-west-2c.pin._zone.hdls.testns.svc.cluster.local.", Qtype: dns.TypeTXT,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Ns: []dns.RR{
|
||||
test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 5"),
|
||||
},
|
||||
},
|
||||
{ // prefer: SRV narrows when populated and falls back when not, same as A
|
||||
Qname: "us-west-2c.prefer._zone.hdls.testns.svc.cluster.local.", Qtype: dns.TypeSRV,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Answer: []dns.RR{
|
||||
test.SRV("us-west-2c.prefer._zone.hdls.testns.svc.cluster.local. 5 IN SRV 0 33 80 172-0-0-1.hdls.testns.svc.cluster.local."),
|
||||
test.SRV("us-west-2c.prefer._zone.hdls.testns.svc.cluster.local. 5 IN SRV 0 33 80 172-0-0-2.hdls.testns.svc.cluster.local."),
|
||||
test.SRV("us-west-2c.prefer._zone.hdls.testns.svc.cluster.local. 5 IN SRV 0 33 80 172-0-0-3.hdls.testns.svc.cluster.local."),
|
||||
},
|
||||
Extra: []dns.RR{
|
||||
test.A("172-0-0-1.hdls.testns.svc.cluster.local. 5 IN A 172.0.0.1"),
|
||||
test.A("172-0-0-2.hdls.testns.svc.cluster.local. 5 IN A 172.0.0.2"),
|
||||
test.A("172-0-0-3.hdls.testns.svc.cluster.local. 5 IN A 172.0.0.3"),
|
||||
},
|
||||
},
|
||||
{ // prefer: TXT on a zonal name is NODATA like every non-address type
|
||||
Qname: "us-west-2c.prefer._zone.hdls.testns.svc.cluster.local.", Qtype: dns.TypeTXT,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Ns: []dns.RR{
|
||||
test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 5"),
|
||||
},
|
||||
},
|
||||
{ // pin: SRV comes out zone-filtered, since filtering happens at endpoint selection
|
||||
Qname: "us-west-2b.pin._zone.hdls.testns.svc.cluster.local.", Qtype: dns.TypeSRV,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Answer: []dns.RR{
|
||||
test.SRV("us-west-2b.pin._zone.hdls.testns.svc.cluster.local. 5 IN SRV 0 50 80 172-0-0-2.hdls.testns.svc.cluster.local."),
|
||||
test.SRV("us-west-2b.pin._zone.hdls.testns.svc.cluster.local. 5 IN SRV 0 50 80 172-0-0-3.hdls.testns.svc.cluster.local."),
|
||||
},
|
||||
Extra: []dns.RR{
|
||||
test.A("172-0-0-2.hdls.testns.svc.cluster.local. 5 IN A 172.0.0.2"),
|
||||
test.A("172-0-0-3.hdls.testns.svc.cluster.local. 5 IN A 172.0.0.3"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Without the option the shape is three-labels-too-long, exactly as it has
|
||||
// always been: behavior identical to before the feature existed.
|
||||
var zonalDisabledTestCases = []test.Case{
|
||||
{
|
||||
Qname: "us-west-2a.pin._zone.hdls.testns.svc.cluster.local.", Qtype: dns.TypeA,
|
||||
Rcode: dns.RcodeNameError,
|
||||
Ns: []dns.RR{
|
||||
test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 5"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// A zone-only endpoint change alters zonal answers, so it must not be
|
||||
// classified as equivalent (which would skip the serial bump). Zone is
|
||||
// empty in default configurations, so this costs nothing when the option
|
||||
// is off.
|
||||
func TestEndpointsEquivalentZoneChange(t *testing.T) {
|
||||
eps := func(zone string) *object.Endpoints {
|
||||
return &object.Endpoints{
|
||||
Subsets: []object.EndpointSubset{{
|
||||
Addresses: []object.EndpointAddress{{IP: "172.0.0.1"}},
|
||||
}},
|
||||
Zones: map[string]string{"172.0.0.1": zone},
|
||||
}
|
||||
}
|
||||
if !endpointsEquivalent(eps("us-west-2a"), eps("us-west-2a")) {
|
||||
t.Fatal("identical endpoints must be equivalent")
|
||||
}
|
||||
if endpointsEquivalent(eps("us-west-2a"), eps("us-west-2b")) {
|
||||
t.Fatal("a zone-only change alters zonal answers and must not be equivalent")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeDNSZonal(t *testing.T) {
|
||||
k := New([]string{"cluster.local."})
|
||||
k.APIConn = &APIConnZonalTest{}
|
||||
k.Next = test.NextHandler(dns.RcodeSuccess, nil)
|
||||
k.Namespaces = map[string]struct{}{"testns": {}}
|
||||
k.opts.zonal = true
|
||||
ctx := context.TODO()
|
||||
|
||||
runZonalCases(ctx, t, k, zonalTestCases)
|
||||
|
||||
k.opts.zonal = false
|
||||
runZonalCases(ctx, t, k, zonalDisabledTestCases)
|
||||
}
|
||||
|
||||
func runZonalCases(ctx context.Context, t *testing.T, k *Kubernetes, cases []test.Case) {
|
||||
t.Helper()
|
||||
for i, tc := range cases {
|
||||
r := tc.Msg()
|
||||
w := dnstest.NewRecorder(&test.ResponseWriter{})
|
||||
if _, err := k.ServeDNS(ctx, w, r); err != nil {
|
||||
t.Errorf("Test %d expected no error, got %v", i, err)
|
||||
continue
|
||||
}
|
||||
if w.Msg == nil {
|
||||
t.Fatalf("Test %d, got nil message for %q", i, r.Question[0].Name)
|
||||
}
|
||||
if err := test.SortAndCheck(w.Msg, tc); err != nil {
|
||||
t.Errorf("Test %d (%s), %v", i, tc.Qname, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user