mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-10-31 00:01:40 +01:00 
			
		
		
		
	This change adds full IPv6 support to the Kubernetes operator's DNS functionality, enabling dual-stack and IPv6-only cluster support. Fixes #16633 Signed-off-by: Raj Singh <raj@tailscale.com>
		
			
				
	
	
		
			504 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			504 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright (c) Tailscale Inc & AUTHORS
 | |
| // SPDX-License-Identifier: BSD-3-Clause
 | |
| 
 | |
| //go:build !plan9
 | |
| 
 | |
| package main
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"encoding/json"
 | |
| 	"fmt"
 | |
| 	"testing"
 | |
| 
 | |
| 	"github.com/google/go-cmp/cmp"
 | |
| 	"go.uber.org/zap"
 | |
| 	corev1 "k8s.io/api/core/v1"
 | |
| 	discoveryv1 "k8s.io/api/discovery/v1"
 | |
| 	networkingv1 "k8s.io/api/networking/v1"
 | |
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 | |
| 	"k8s.io/apimachinery/pkg/types"
 | |
| 	"k8s.io/apimachinery/pkg/util/intstr"
 | |
| 	"sigs.k8s.io/controller-runtime/pkg/client"
 | |
| 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 | |
| 	operatorutils "tailscale.com/k8s-operator"
 | |
| 	tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
 | |
| 	"tailscale.com/kube/kubetypes"
 | |
| 	"tailscale.com/tstest"
 | |
| 	"tailscale.com/types/ptr"
 | |
| )
 | |
| 
 | |
| func TestDNSRecordsReconciler(t *testing.T) {
 | |
| 	// Preconfigure a cluster with a DNSConfig
 | |
| 	dnsConfig := &tsapi.DNSConfig{
 | |
| 		ObjectMeta: metav1.ObjectMeta{
 | |
| 			Name: "test",
 | |
| 		},
 | |
| 		TypeMeta: metav1.TypeMeta{Kind: "DNSConfig"},
 | |
| 		Spec: tsapi.DNSConfigSpec{
 | |
| 			Nameserver: &tsapi.Nameserver{},
 | |
| 		}}
 | |
| 	ing := &networkingv1.Ingress{
 | |
| 		ObjectMeta: metav1.ObjectMeta{
 | |
| 			Name:      "ts-ingress",
 | |
| 			Namespace: "test",
 | |
| 		},
 | |
| 		Spec: networkingv1.IngressSpec{
 | |
| 			IngressClassName: ptr.To("tailscale"),
 | |
| 		},
 | |
| 		Status: networkingv1.IngressStatus{
 | |
| 			LoadBalancer: networkingv1.IngressLoadBalancerStatus{
 | |
| 				Ingress: []networkingv1.IngressLoadBalancerIngress{{
 | |
| 					Hostname: "cluster.ingress.ts.net"}},
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 	cm := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "dnsrecords", Namespace: "tailscale"}}
 | |
| 	fc := fake.NewClientBuilder().
 | |
| 		WithScheme(tsapi.GlobalScheme).
 | |
| 		WithObjects(cm).
 | |
| 		WithObjects(dnsConfig).
 | |
| 		WithObjects(ing).
 | |
| 		WithStatusSubresource(dnsConfig, ing).
 | |
| 		Build()
 | |
| 	zl, err := zap.NewDevelopment()
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	cl := tstest.NewClock(tstest.ClockOpts{})
 | |
| 	// Set the ready condition of the DNSConfig
 | |
| 	mustUpdateStatus(t, fc, "", "test", func(c *tsapi.DNSConfig) {
 | |
| 		operatorutils.SetDNSConfigCondition(c, tsapi.NameserverReady, metav1.ConditionTrue, reasonNameserverCreated, reasonNameserverCreated, 0, cl, zl.Sugar())
 | |
| 	})
 | |
| 	dnsRR := &dnsRecordsReconciler{
 | |
| 		Client:      fc,
 | |
| 		logger:      zl.Sugar(),
 | |
| 		tsNamespace: "tailscale",
 | |
| 	}
 | |
| 
 | |
| 	// 1. DNS record is created for an egress proxy configured via
 | |
| 	// tailscale.com/tailnet-fqdn annotation
 | |
| 	egressSvcFQDN := &corev1.Service{
 | |
| 		ObjectMeta: metav1.ObjectMeta{
 | |
| 			Name:        "egress-fqdn",
 | |
| 			Namespace:   "test",
 | |
| 			Annotations: map[string]string{"tailscale.com/tailnet-fqdn": "foo.bar.ts.net"},
 | |
| 		},
 | |
| 		Spec: corev1.ServiceSpec{
 | |
| 			ExternalName: "unused",
 | |
| 			Type:         corev1.ServiceTypeExternalName,
 | |
| 		},
 | |
| 	}
 | |
| 	headlessForEgressSvcFQDN := headlessSvcForParent(egressSvcFQDN, "svc") // create the proxy headless Service
 | |
| 	ep := endpointSliceForService(headlessForEgressSvcFQDN, "10.9.8.7", discoveryv1.AddressTypeIPv4)
 | |
| 	epv6 := endpointSliceForService(headlessForEgressSvcFQDN, "2600:1900:4011:161:0:d:0:d", discoveryv1.AddressTypeIPv6)
 | |
| 
 | |
| 	mustCreate(t, fc, egressSvcFQDN)
 | |
| 	mustCreate(t, fc, headlessForEgressSvcFQDN)
 | |
| 	mustCreate(t, fc, ep)
 | |
| 	mustCreate(t, fc, epv6)
 | |
| 	expectReconciled(t, dnsRR, "tailscale", "egress-fqdn") // dns-records-reconciler reconcile the headless Service
 | |
| 	// ConfigMap should now have a record for foo.bar.ts.net -> 10.8.8.7
 | |
| 	wantHosts := map[string][]string{"foo.bar.ts.net": {"10.9.8.7"}}
 | |
| 	wantHostsIPv6 := map[string][]string{"foo.bar.ts.net": {"2600:1900:4011:161:0:d:0:d"}}
 | |
| 	expectHostsRecordsWithIPv6(t, fc, wantHosts, wantHostsIPv6)
 | |
| 
 | |
| 	// 2. DNS record is updated if tailscale.com/tailnet-fqdn annotation's
 | |
| 	// value changes
 | |
| 	mustUpdate(t, fc, "test", "egress-fqdn", func(svc *corev1.Service) {
 | |
| 		svc.Annotations["tailscale.com/tailnet-fqdn"] = "baz.bar.ts.net"
 | |
| 	})
 | |
| 	expectReconciled(t, dnsRR, "tailscale", "egress-fqdn") // dns-records-reconciler reconcile the headless Service
 | |
| 	wantHosts = map[string][]string{"baz.bar.ts.net": {"10.9.8.7"}}
 | |
| 	expectHostsRecords(t, fc, wantHosts)
 | |
| 
 | |
| 	// 3. DNS record is updated if the IP address of the proxy Pod changes.
 | |
| 	ep = endpointSliceForService(headlessForEgressSvcFQDN, "10.6.5.4", discoveryv1.AddressTypeIPv4)
 | |
| 	mustUpdate(t, fc, ep.Namespace, ep.Name, func(ep *discoveryv1.EndpointSlice) {
 | |
| 		ep.Endpoints[0].Addresses = []string{"10.6.5.4"}
 | |
| 	})
 | |
| 	expectReconciled(t, dnsRR, "tailscale", "egress-fqdn") // dns-records-reconciler reconcile the headless Service
 | |
| 	wantHosts = map[string][]string{"baz.bar.ts.net": {"10.6.5.4"}}
 | |
| 	expectHostsRecords(t, fc, wantHosts)
 | |
| 
 | |
| 	// 4. DNS record is created for an ingress proxy configured via Ingress
 | |
| 	headlessForIngress := headlessSvcForParent(ing, "ingress")
 | |
| 	ep = endpointSliceForService(headlessForIngress, "10.9.8.7", discoveryv1.AddressTypeIPv4)
 | |
| 	mustCreate(t, fc, headlessForIngress)
 | |
| 	mustCreate(t, fc, ep)
 | |
| 	expectReconciled(t, dnsRR, "tailscale", "ts-ingress") // dns-records-reconciler should reconcile the headless Service
 | |
| 	wantHosts["cluster.ingress.ts.net"] = []string{"10.9.8.7"}
 | |
| 	expectHostsRecords(t, fc, wantHosts)
 | |
| 
 | |
| 	// 5. DNS records are updated if Ingress's MagicDNS name changes (i.e users changed spec.tls.hosts[0])
 | |
| 	t.Log("test case 5")
 | |
| 	mustUpdateStatus(t, fc, "test", "ts-ingress", func(ing *networkingv1.Ingress) {
 | |
| 		ing.Status.LoadBalancer.Ingress[0].Hostname = "another.ingress.ts.net"
 | |
| 	})
 | |
| 	expectReconciled(t, dnsRR, "tailscale", "ts-ingress") // dns-records-reconciler should reconcile the headless Service
 | |
| 	delete(wantHosts, "cluster.ingress.ts.net")
 | |
| 	wantHosts["another.ingress.ts.net"] = []string{"10.9.8.7"}
 | |
| 	expectHostsRecords(t, fc, wantHosts)
 | |
| 
 | |
| 	// 6. DNS records are updated if Ingress proxy's Pod IP changes
 | |
| 	mustUpdate(t, fc, ep.Namespace, ep.Name, func(ep *discoveryv1.EndpointSlice) {
 | |
| 		ep.Endpoints[0].Addresses = []string{"7.8.9.10"}
 | |
| 	})
 | |
| 	expectReconciled(t, dnsRR, "tailscale", "ts-ingress")
 | |
| 	wantHosts["another.ingress.ts.net"] = []string{"7.8.9.10"}
 | |
| 	expectHostsRecords(t, fc, wantHosts)
 | |
| 
 | |
| 	// 7. A not-ready Endpoint is removed from DNS config.
 | |
| 	mustUpdate(t, fc, ep.Namespace, ep.Name, func(ep *discoveryv1.EndpointSlice) {
 | |
| 		ep.Endpoints[0].Conditions.Ready = ptr.To(false)
 | |
| 		ep.Endpoints = append(ep.Endpoints, discoveryv1.Endpoint{
 | |
| 			Addresses: []string{"1.2.3.4"},
 | |
| 		})
 | |
| 	})
 | |
| 	expectReconciled(t, dnsRR, "tailscale", "ts-ingress")
 | |
| 	wantHosts["another.ingress.ts.net"] = []string{"1.2.3.4"}
 | |
| 	expectHostsRecords(t, fc, wantHosts)
 | |
| 
 | |
| 	// 8. DNS record is created for ProxyGroup egress using ClusterIP Service IP instead of Pod IPs
 | |
| 	t.Log("test case 8: ProxyGroup egress")
 | |
| 
 | |
| 	// Create the parent ExternalName service with tailnet-fqdn annotation
 | |
| 	parentEgressSvc := &corev1.Service{
 | |
| 		ObjectMeta: metav1.ObjectMeta{
 | |
| 			Name:      "external-service",
 | |
| 			Namespace: "default",
 | |
| 			Annotations: map[string]string{
 | |
| 				AnnotationTailnetTargetFQDN: "external-service.example.ts.net",
 | |
| 			},
 | |
| 		},
 | |
| 		Spec: corev1.ServiceSpec{
 | |
| 			Type:         corev1.ServiceTypeExternalName,
 | |
| 			ExternalName: "unused",
 | |
| 		},
 | |
| 	}
 | |
| 	mustCreate(t, fc, parentEgressSvc)
 | |
| 
 | |
| 	proxyGroupEgressSvc := &corev1.Service{
 | |
| 		ObjectMeta: metav1.ObjectMeta{
 | |
| 			Name:      "ts-proxygroup-egress-abcd1",
 | |
| 			Namespace: "tailscale",
 | |
| 			Labels: map[string]string{
 | |
| 				kubetypes.LabelManaged: "true",
 | |
| 				LabelParentName:        "external-service",
 | |
| 				LabelParentNamespace:   "default",
 | |
| 				LabelParentType:        "svc",
 | |
| 				labelProxyGroup:        "test-proxy-group",
 | |
| 				labelSvcType:           typeEgress,
 | |
| 			},
 | |
| 		},
 | |
| 		Spec: corev1.ServiceSpec{
 | |
| 			Type:      corev1.ServiceTypeClusterIP,
 | |
| 			ClusterIP: "10.0.100.50", // This IP should be used in DNS, not Pod IPs
 | |
| 			Ports: []corev1.ServicePort{{
 | |
| 				Port:       443,
 | |
| 				TargetPort: intstr.FromInt(10443), // Port mapping
 | |
| 			}},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	// Create EndpointSlice with Pod IPs (these should NOT be used in DNS records)
 | |
| 	proxyGroupEps := &discoveryv1.EndpointSlice{
 | |
| 		ObjectMeta: metav1.ObjectMeta{
 | |
| 			Name:      "ts-proxygroup-egress-abcd1-ipv4",
 | |
| 			Namespace: "tailscale",
 | |
| 			Labels: map[string]string{
 | |
| 				discoveryv1.LabelServiceName: "ts-proxygroup-egress-abcd1",
 | |
| 				kubetypes.LabelManaged:       "true",
 | |
| 				LabelParentName:              "external-service",
 | |
| 				LabelParentNamespace:         "default",
 | |
| 				LabelParentType:              "svc",
 | |
| 				labelProxyGroup:              "test-proxy-group",
 | |
| 				labelSvcType:                 typeEgress,
 | |
| 			},
 | |
| 		},
 | |
| 		AddressType: discoveryv1.AddressTypeIPv4,
 | |
| 		Endpoints: []discoveryv1.Endpoint{{
 | |
| 			Addresses: []string{"10.1.0.100", "10.1.0.101", "10.1.0.102"}, // Pod IPs that should NOT be used
 | |
| 			Conditions: discoveryv1.EndpointConditions{
 | |
| 				Ready:       ptr.To(true),
 | |
| 				Serving:     ptr.To(true),
 | |
| 				Terminating: ptr.To(false),
 | |
| 			},
 | |
| 		}},
 | |
| 		Ports: []discoveryv1.EndpointPort{{
 | |
| 			Port: ptr.To(int32(10443)),
 | |
| 		}},
 | |
| 	}
 | |
| 
 | |
| 	mustCreate(t, fc, proxyGroupEgressSvc)
 | |
| 	mustCreate(t, fc, proxyGroupEps)
 | |
| 	expectReconciled(t, dnsRR, "tailscale", "ts-proxygroup-egress-abcd1")
 | |
| 
 | |
| 	// Verify DNS record uses ClusterIP Service IP, not Pod IPs
 | |
| 	wantHosts["external-service.example.ts.net"] = []string{"10.0.100.50"}
 | |
| 	expectHostsRecords(t, fc, wantHosts)
 | |
| 
 | |
| 	// 9. ProxyGroup egress DNS record updates when ClusterIP changes
 | |
| 	t.Log("test case 9: ProxyGroup egress ClusterIP change")
 | |
| 	mustUpdate(t, fc, "tailscale", "ts-proxygroup-egress-abcd1", func(svc *corev1.Service) {
 | |
| 		svc.Spec.ClusterIP = "10.0.100.51"
 | |
| 	})
 | |
| 	expectReconciled(t, dnsRR, "tailscale", "ts-proxygroup-egress-abcd1")
 | |
| 	wantHosts["external-service.example.ts.net"] = []string{"10.0.100.51"}
 | |
| 	expectHostsRecords(t, fc, wantHosts)
 | |
| 
 | |
| 	// 10. Test ProxyGroup service deletion and DNS cleanup
 | |
| 	t.Log("test case 10: ProxyGroup egress service deletion")
 | |
| 	mustDeleteAll(t, fc, proxyGroupEgressSvc)
 | |
| 	expectReconciled(t, dnsRR, "tailscale", "ts-proxygroup-egress-abcd1")
 | |
| 	delete(wantHosts, "external-service.example.ts.net")
 | |
| 	expectHostsRecords(t, fc, wantHosts)
 | |
| }
 | |
| 
 | |
| func TestDNSRecordsReconcilerErrorCases(t *testing.T) {
 | |
| 	zl, err := zap.NewDevelopment()
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 
 | |
| 	dnsRR := &dnsRecordsReconciler{
 | |
| 		logger: zl.Sugar(),
 | |
| 	}
 | |
| 
 | |
| 	testSvc := &corev1.Service{
 | |
| 		ObjectMeta: metav1.ObjectMeta{Name: "test"},
 | |
| 		Spec:       corev1.ServiceSpec{Type: corev1.ServiceTypeClusterIP},
 | |
| 	}
 | |
| 
 | |
| 	// Test invalid IP format
 | |
| 	testSvc.Spec.ClusterIP = "invalid-ip"
 | |
| 	_, _, err = dnsRR.getClusterIPServiceIPs(testSvc, zl.Sugar())
 | |
| 	if err == nil {
 | |
| 		t.Error("expected error for invalid IP format")
 | |
| 	}
 | |
| 
 | |
| 	// Test valid IP
 | |
| 	testSvc.Spec.ClusterIP = "10.0.100.50"
 | |
| 	ip4s, ip6s, err := dnsRR.getClusterIPServiceIPs(testSvc, zl.Sugar())
 | |
| 	if err != nil {
 | |
| 		t.Errorf("unexpected error for valid IP: %v", err)
 | |
| 	}
 | |
| 	if len(ip4s) != 1 || ip4s[0] != "10.0.100.50" {
 | |
| 		t.Errorf("expected IPv4 address 10.0.100.50, got %v", ip4s)
 | |
| 	}
 | |
| 	if len(ip6s) != 0 {
 | |
| 		t.Errorf("expected no IPv6 addresses, got %v", ip6s)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestDNSRecordsReconcilerDualStack(t *testing.T) {
 | |
| 	// Test dual-stack (IPv4 and IPv6) scenarios
 | |
| 	zl, err := zap.NewDevelopment()
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 
 | |
| 	// Preconfigure cluster with DNSConfig
 | |
| 	dnsCfg := &tsapi.DNSConfig{
 | |
| 		ObjectMeta: metav1.ObjectMeta{Name: "test"},
 | |
| 		TypeMeta:   metav1.TypeMeta{Kind: "DNSConfig"},
 | |
| 		Spec:       tsapi.DNSConfigSpec{Nameserver: &tsapi.Nameserver{}},
 | |
| 	}
 | |
| 	dnsCfg.Status.Conditions = append(dnsCfg.Status.Conditions, metav1.Condition{
 | |
| 		Type:   string(tsapi.NameserverReady),
 | |
| 		Status: metav1.ConditionTrue,
 | |
| 	})
 | |
| 
 | |
| 	// Create dual-stack ingress
 | |
| 	ing := &networkingv1.Ingress{
 | |
| 		ObjectMeta: metav1.ObjectMeta{
 | |
| 			Name:      "dual-stack-ingress",
 | |
| 			Namespace: "test",
 | |
| 		},
 | |
| 		Spec: networkingv1.IngressSpec{
 | |
| 			IngressClassName: ptr.To("tailscale"),
 | |
| 		},
 | |
| 		Status: networkingv1.IngressStatus{
 | |
| 			LoadBalancer: networkingv1.IngressLoadBalancerStatus{
 | |
| 				Ingress: []networkingv1.IngressLoadBalancerIngress{
 | |
| 					{Hostname: "dual-stack.example.ts.net"},
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	headlessSvc := headlessSvcForParent(ing, "ingress")
 | |
| 	headlessSvc.Name = "ts-dual-stack-ingress"
 | |
| 	headlessSvc.SetLabels(map[string]string{
 | |
| 		kubetypes.LabelManaged: "true",
 | |
| 		LabelParentName:        "dual-stack-ingress",
 | |
| 		LabelParentNamespace:   "test",
 | |
| 		LabelParentType:        "ingress",
 | |
| 	})
 | |
| 
 | |
| 	// Create both IPv4 and IPv6 endpoints
 | |
| 	epv4 := endpointSliceForService(headlessSvc, "10.1.2.3", discoveryv1.AddressTypeIPv4)
 | |
| 	epv6 := endpointSliceForService(headlessSvc, "2001:db8::1", discoveryv1.AddressTypeIPv6)
 | |
| 
 | |
| 	dnsRRDualStack := &dnsRecordsReconciler{
 | |
| 		tsNamespace: "tailscale",
 | |
| 		logger:      zl.Sugar(),
 | |
| 	}
 | |
| 
 | |
| 	// Create the dnsrecords ConfigMap
 | |
| 	cm := &corev1.ConfigMap{
 | |
| 		ObjectMeta: metav1.ObjectMeta{
 | |
| 			Name:      operatorutils.DNSRecordsCMName,
 | |
| 			Namespace: "tailscale",
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	fc := fake.NewClientBuilder().
 | |
| 		WithScheme(tsapi.GlobalScheme).
 | |
| 		WithObjects(dnsCfg, ing, headlessSvc, epv4, epv6, cm).
 | |
| 		WithStatusSubresource(dnsCfg).
 | |
| 		Build()
 | |
| 
 | |
| 	dnsRRDualStack.Client = fc
 | |
| 
 | |
| 	// Test dual-stack service records
 | |
| 	expectReconciled(t, dnsRRDualStack, "tailscale", "ts-dual-stack-ingress")
 | |
| 
 | |
| 	wantIPv4 := map[string][]string{"dual-stack.example.ts.net": {"10.1.2.3"}}
 | |
| 	wantIPv6 := map[string][]string{"dual-stack.example.ts.net": {"2001:db8::1"}}
 | |
| 	expectHostsRecordsWithIPv6(t, fc, wantIPv4, wantIPv6)
 | |
| 
 | |
| 	// Test ProxyGroup with dual-stack ClusterIPs
 | |
| 	// First create parent service
 | |
| 	parentEgressSvc := &corev1.Service{
 | |
| 		ObjectMeta: metav1.ObjectMeta{
 | |
| 			Name:      "pg-service",
 | |
| 			Namespace: "tailscale",
 | |
| 			Annotations: map[string]string{
 | |
| 				AnnotationTailnetTargetFQDN: "pg-service.example.ts.net",
 | |
| 			},
 | |
| 		},
 | |
| 		Spec: corev1.ServiceSpec{
 | |
| 			Type:         corev1.ServiceTypeExternalName,
 | |
| 			ExternalName: "unused",
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	proxyGroupSvc := &corev1.Service{
 | |
| 		ObjectMeta: metav1.ObjectMeta{
 | |
| 			Name:      "ts-proxygroup-dualstack",
 | |
| 			Namespace: "tailscale",
 | |
| 			Labels: map[string]string{
 | |
| 				kubetypes.LabelManaged: "true",
 | |
| 				labelProxyGroup:        "test-pg",
 | |
| 				labelSvcType:           typeEgress,
 | |
| 				LabelParentName:        "pg-service",
 | |
| 				LabelParentNamespace:   "tailscale",
 | |
| 				LabelParentType:        "svc",
 | |
| 			},
 | |
| 			Annotations: map[string]string{
 | |
| 				annotationTSMagicDNSName: "pg-service.example.ts.net",
 | |
| 			},
 | |
| 		},
 | |
| 		Spec: corev1.ServiceSpec{
 | |
| 			Type:       corev1.ServiceTypeClusterIP,
 | |
| 			ClusterIP:  "10.96.0.100",
 | |
| 			ClusterIPs: []string{"10.96.0.100", "2001:db8::100"},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	mustCreate(t, fc, parentEgressSvc)
 | |
| 	mustCreate(t, fc, proxyGroupSvc)
 | |
| 	expectReconciled(t, dnsRRDualStack, "tailscale", "ts-proxygroup-dualstack")
 | |
| 
 | |
| 	wantIPv4["pg-service.example.ts.net"] = []string{"10.96.0.100"}
 | |
| 	wantIPv6["pg-service.example.ts.net"] = []string{"2001:db8::100"}
 | |
| 	expectHostsRecordsWithIPv6(t, fc, wantIPv4, wantIPv6)
 | |
| }
 | |
| 
 | |
| func headlessSvcForParent(o client.Object, typ string) *corev1.Service {
 | |
| 	return &corev1.Service{
 | |
| 		ObjectMeta: metav1.ObjectMeta{
 | |
| 			Name:      o.GetName(),
 | |
| 			Namespace: "tailscale",
 | |
| 			Labels: map[string]string{
 | |
| 				kubetypes.LabelManaged: "true",
 | |
| 				LabelParentName:        o.GetName(),
 | |
| 				LabelParentNamespace:   o.GetNamespace(),
 | |
| 				LabelParentType:        typ,
 | |
| 			},
 | |
| 		},
 | |
| 		Spec: corev1.ServiceSpec{
 | |
| 			ClusterIP: "None",
 | |
| 			Type:      corev1.ServiceTypeClusterIP,
 | |
| 			Selector:  map[string]string{"foo": "bar"},
 | |
| 		},
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func endpointSliceForService(svc *corev1.Service, ip string, fam discoveryv1.AddressType) *discoveryv1.EndpointSlice {
 | |
| 	return &discoveryv1.EndpointSlice{
 | |
| 		ObjectMeta: metav1.ObjectMeta{
 | |
| 			Name:      fmt.Sprintf("%s-%s", svc.Name, string(fam)),
 | |
| 			Namespace: svc.Namespace,
 | |
| 			Labels:    map[string]string{discoveryv1.LabelServiceName: svc.Name},
 | |
| 		},
 | |
| 		AddressType: fam,
 | |
| 		Endpoints: []discoveryv1.Endpoint{{
 | |
| 			Addresses: []string{ip},
 | |
| 			Conditions: discoveryv1.EndpointConditions{
 | |
| 				Ready:       ptr.To(true),
 | |
| 				Serving:     ptr.To(true),
 | |
| 				Terminating: ptr.To(false),
 | |
| 			},
 | |
| 		}},
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func expectHostsRecords(t *testing.T, cl client.Client, wantsHosts map[string][]string) {
 | |
| 	t.Helper()
 | |
| 	cm := new(corev1.ConfigMap)
 | |
| 	if err := cl.Get(context.Background(), types.NamespacedName{Name: "dnsrecords", Namespace: "tailscale"}, cm); err != nil {
 | |
| 		t.Fatalf("getting dnsconfig ConfigMap: %v", err)
 | |
| 	}
 | |
| 	if cm.Data == nil {
 | |
| 		t.Fatal("dnsconfig ConfigMap has no data")
 | |
| 	}
 | |
| 	dnsConfigString, ok := cm.Data[operatorutils.DNSRecordsCMKey]
 | |
| 	if !ok {
 | |
| 		t.Fatal("dnsconfig ConfigMap does not contain dnsconfig")
 | |
| 	}
 | |
| 	dnsConfig := &operatorutils.Records{}
 | |
| 	if err := json.Unmarshal([]byte(dnsConfigString), dnsConfig); err != nil {
 | |
| 		t.Fatalf("unmarshaling dnsconfig: %v", err)
 | |
| 	}
 | |
| 	if diff := cmp.Diff(dnsConfig.IP4, wantsHosts); diff != "" {
 | |
| 		t.Fatalf("unexpected dns config (-got +want):\n%s", diff)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func expectHostsRecordsWithIPv6(t *testing.T, cl client.Client, wantsHostsIPv4, wantsHostsIPv6 map[string][]string) {
 | |
| 	t.Helper()
 | |
| 	cm := new(corev1.ConfigMap)
 | |
| 	if err := cl.Get(context.Background(), types.NamespacedName{Name: "dnsrecords", Namespace: "tailscale"}, cm); err != nil {
 | |
| 		t.Fatalf("getting dnsconfig ConfigMap: %v", err)
 | |
| 	}
 | |
| 	if cm.Data == nil {
 | |
| 		t.Fatal("dnsconfig ConfigMap has no data")
 | |
| 	}
 | |
| 	dnsConfigString, ok := cm.Data[operatorutils.DNSRecordsCMKey]
 | |
| 	if !ok {
 | |
| 		t.Fatal("dnsconfig ConfigMap does not contain dnsconfig")
 | |
| 	}
 | |
| 	dnsConfig := &operatorutils.Records{}
 | |
| 	if err := json.Unmarshal([]byte(dnsConfigString), dnsConfig); err != nil {
 | |
| 		t.Fatalf("unmarshaling dnsconfig: %v", err)
 | |
| 	}
 | |
| 	if diff := cmp.Diff(dnsConfig.IP4, wantsHostsIPv4); diff != "" {
 | |
| 		t.Fatalf("unexpected IPv4 dns config (-got +want):\n%s", diff)
 | |
| 	}
 | |
| 	if diff := cmp.Diff(dnsConfig.IP6, wantsHostsIPv6); diff != "" {
 | |
| 		t.Fatalf("unexpected IPv6 dns config (-got +want):\n%s", diff)
 | |
| 	}
 | |
| }
 |