// Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause //go:build !plan9 package main import ( "fmt" "testing" "go.uber.org/zap" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client/fake" "tailscale.com/types/ptr" ) func TestLoadBalancerClass(t *testing.T) { fc := fake.NewFakeClient() ft := &fakeTSClient{} zl, err := zap.NewDevelopment() if err != nil { t.Fatal(err) } sr := &ServiceReconciler{ Client: fc, ssr: &tailscaleSTSReconciler{ Client: fc, tsClient: ft, defaultTags: []string{"tag:k8s"}, operatorNamespace: "operator-ns", proxyImage: "tailscale/tailscale", }, logger: zl.Sugar(), } // Create a service that we should manage, and check that the initial round // of objects looks right. mustCreate(t, fc, &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test", Namespace: "default", // The apiserver is supposed to set the UID, but the fake client // doesn't. So, set it explicitly because other code later depends // on it being set. UID: types.UID("1234-UID"), }, Spec: corev1.ServiceSpec{ ClusterIP: "10.20.30.40", Type: corev1.ServiceTypeLoadBalancer, LoadBalancerClass: ptr.To("tailscale"), }, }) expectReconciled(t, sr, "default", "test") fullName, shortName := findGenName(t, fc, "default", "test", "svc") opts := configOpts{ stsName: shortName, secretName: fullName, namespace: "default", parentType: "svc", hostname: "default-test", clusterTargetIP: "10.20.30.40", } expectEqual(t, fc, expectedSecret(t, opts)) expectEqual(t, fc, expectedHeadlessService(shortName)) expectEqual(t, fc, expectedSTS(opts)) // Normally the Tailscale proxy pod would come up here and write its info // into the secret. Simulate that, then verify reconcile again and verify // that we get to the end. mustUpdate(t, fc, "operator-ns", fullName, func(s *corev1.Secret) { if s.Data == nil { s.Data = map[string][]byte{} } s.Data["device_id"] = []byte("ts-id-1234") s.Data["device_fqdn"] = []byte("tailscale.device.name.") s.Data["device_ips"] = []byte(`["100.99.98.97", "2c0a:8083:94d4:2012:3165:34a5:3616:5fdf"]`) }) expectReconciled(t, sr, "default", "test") want := &corev1.Service{ TypeMeta: metav1.TypeMeta{ Kind: "Service", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "test", Namespace: "default", Finalizers: []string{"tailscale.com/finalizer"}, UID: types.UID("1234-UID"), }, Spec: corev1.ServiceSpec{ ClusterIP: "10.20.30.40", Type: corev1.ServiceTypeLoadBalancer, LoadBalancerClass: ptr.To("tailscale"), }, Status: corev1.ServiceStatus{ LoadBalancer: corev1.LoadBalancerStatus{ Ingress: []corev1.LoadBalancerIngress{ { Hostname: "tailscale.device.name", }, { IP: "100.99.98.97", }, }, }, }, } expectEqual(t, fc, want) // Turn the service back into a ClusterIP service, which should make the // operator clean up. mustUpdate(t, fc, "default", "test", func(s *corev1.Service) { s.Spec.Type = corev1.ServiceTypeClusterIP s.Spec.LoadBalancerClass = nil }) mustUpdateStatus(t, fc, "default", "test", func(s *corev1.Service) { // Fake client doesn't automatically delete the LoadBalancer status when // changing away from the LoadBalancer type, we have to do // controller-manager's work by hand. s.Status = corev1.ServiceStatus{} }) // synchronous StatefulSet deletion triggers a requeue. But, the StatefulSet // didn't create any child resources since this is all faked, so the // deletion goes through immediately. expectReconciled(t, sr, "default", "test") expectMissing[appsv1.StatefulSet](t, fc, "operator-ns", shortName) // The deletion triggers another reconcile, to finish the cleanup. expectReconciled(t, sr, "default", "test") expectMissing[appsv1.StatefulSet](t, fc, "operator-ns", shortName) expectMissing[corev1.Service](t, fc, "operator-ns", shortName) expectMissing[corev1.Secret](t, fc, "operator-ns", fullName) want = &corev1.Service{ TypeMeta: metav1.TypeMeta{ Kind: "Service", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "test", Namespace: "default", UID: types.UID("1234-UID"), }, Spec: corev1.ServiceSpec{ ClusterIP: "10.20.30.40", Type: corev1.ServiceTypeClusterIP, }, } expectEqual(t, fc, want) } func TestTailnetTargetFQDNAnnotation(t *testing.T) { fc := fake.NewFakeClient() ft := &fakeTSClient{} zl, err := zap.NewDevelopment() if err != nil { t.Fatal(err) } tailnetTargetFQDN := "foo.bar.ts.net." sr := &ServiceReconciler{ Client: fc, ssr: &tailscaleSTSReconciler{ Client: fc, tsClient: ft, defaultTags: []string{"tag:k8s"}, operatorNamespace: "operator-ns", proxyImage: "tailscale/tailscale", }, logger: zl.Sugar(), } // Create a service that we should manage, and check that the initial round // of objects looks right. mustCreate(t, fc, &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test", Namespace: "default", // The apiserver is supposed to set the UID, but the fake client // doesn't. So, set it explicitly because other code later depends // on it being set. UID: types.UID("1234-UID"), Annotations: map[string]string{ AnnotationTailnetTargetFQDN: tailnetTargetFQDN, }, }, Spec: corev1.ServiceSpec{ Type: corev1.ServiceTypeClusterIP, Selector: map[string]string{ "foo": "bar", }, }, }) expectReconciled(t, sr, "default", "test") fullName, shortName := findGenName(t, fc, "default", "test", "svc") o := configOpts{ stsName: shortName, secretName: fullName, namespace: "default", parentType: "svc", tailnetTargetFQDN: tailnetTargetFQDN, hostname: "default-test", } expectEqual(t, fc, expectedSecret(t, o)) expectEqual(t, fc, expectedHeadlessService(shortName)) expectEqual(t, fc, expectedSTS(o)) want := &corev1.Service{ TypeMeta: metav1.TypeMeta{ Kind: "Service", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "test", Namespace: "default", Finalizers: []string{"tailscale.com/finalizer"}, UID: types.UID("1234-UID"), Annotations: map[string]string{ AnnotationTailnetTargetFQDN: tailnetTargetFQDN, }, }, Spec: corev1.ServiceSpec{ ExternalName: fmt.Sprintf("%s.operator-ns.svc.cluster.local", shortName), Type: corev1.ServiceTypeExternalName, Selector: nil, }, } expectEqual(t, fc, want) expectEqual(t, fc, expectedSecret(t, o)) expectEqual(t, fc, expectedHeadlessService(shortName)) expectEqual(t, fc, expectedSTS(o)) // Change the tailscale-target-fqdn annotation which should update the // StatefulSet tailnetTargetFQDN = "bar.baz.ts.net" mustUpdate(t, fc, "default", "test", func(s *corev1.Service) { s.ObjectMeta.Annotations = map[string]string{ AnnotationTailnetTargetFQDN: tailnetTargetFQDN, } }) // Remove the tailscale-target-fqdn annotation which should make the // operator clean up mustUpdate(t, fc, "default", "test", func(s *corev1.Service) { s.ObjectMeta.Annotations = map[string]string{} }) expectReconciled(t, sr, "default", "test") // // synchronous StatefulSet deletion triggers a requeue. But, the StatefulSet // // didn't create any child resources since this is all faked, so the // // deletion goes through immediately. expectReconciled(t, sr, "default", "test") expectMissing[appsv1.StatefulSet](t, fc, "operator-ns", shortName) // // The deletion triggers another reconcile, to finish the cleanup. expectReconciled(t, sr, "default", "test") expectMissing[appsv1.StatefulSet](t, fc, "operator-ns", shortName) expectMissing[corev1.Service](t, fc, "operator-ns", shortName) expectMissing[corev1.Secret](t, fc, "operator-ns", fullName) } func TestTailnetTargetIPAnnotation(t *testing.T) { fc := fake.NewFakeClient() ft := &fakeTSClient{} zl, err := zap.NewDevelopment() if err != nil { t.Fatal(err) } tailnetTargetIP := "100.66.66.66" sr := &ServiceReconciler{ Client: fc, ssr: &tailscaleSTSReconciler{ Client: fc, tsClient: ft, defaultTags: []string{"tag:k8s"}, operatorNamespace: "operator-ns", proxyImage: "tailscale/tailscale", }, logger: zl.Sugar(), } // Create a service that we should manage, and check that the initial round // of objects looks right. mustCreate(t, fc, &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test", Namespace: "default", // The apiserver is supposed to set the UID, but the fake client // doesn't. So, set it explicitly because other code later depends // on it being set. UID: types.UID("1234-UID"), Annotations: map[string]string{ AnnotationTailnetTargetIP: tailnetTargetIP, }, }, Spec: corev1.ServiceSpec{ Type: corev1.ServiceTypeClusterIP, Selector: map[string]string{ "foo": "bar", }, }, }) expectReconciled(t, sr, "default", "test") fullName, shortName := findGenName(t, fc, "default", "test", "svc") o := configOpts{ stsName: shortName, secretName: fullName, namespace: "default", parentType: "svc", tailnetTargetIP: tailnetTargetIP, hostname: "default-test", } expectEqual(t, fc, expectedSecret(t, o)) expectEqual(t, fc, expectedHeadlessService(shortName)) expectEqual(t, fc, expectedSTS(o)) want := &corev1.Service{ TypeMeta: metav1.TypeMeta{ Kind: "Service", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "test", Namespace: "default", Finalizers: []string{"tailscale.com/finalizer"}, UID: types.UID("1234-UID"), Annotations: map[string]string{ AnnotationTailnetTargetIP: tailnetTargetIP, }, }, Spec: corev1.ServiceSpec{ ExternalName: fmt.Sprintf("%s.operator-ns.svc.cluster.local", shortName), Type: corev1.ServiceTypeExternalName, Selector: nil, }, } expectEqual(t, fc, want) expectEqual(t, fc, expectedSecret(t, o)) expectEqual(t, fc, expectedHeadlessService(shortName)) expectEqual(t, fc, expectedSTS(o)) // Change the tailscale-target-ip annotation which should update the // StatefulSet tailnetTargetIP = "100.77.77.77" mustUpdate(t, fc, "default", "test", func(s *corev1.Service) { s.ObjectMeta.Annotations = map[string]string{ AnnotationTailnetTargetIP: tailnetTargetIP, } }) // Remove the tailscale-target-ip annotation which should make the // operator clean up mustUpdate(t, fc, "default", "test", func(s *corev1.Service) { s.ObjectMeta.Annotations = map[string]string{} }) expectReconciled(t, sr, "default", "test") // // synchronous StatefulSet deletion triggers a requeue. But, the StatefulSet // // didn't create any child resources since this is all faked, so the // // deletion goes through immediately. expectReconciled(t, sr, "default", "test") expectMissing[appsv1.StatefulSet](t, fc, "operator-ns", shortName) // // The deletion triggers another reconcile, to finish the cleanup. expectReconciled(t, sr, "default", "test") expectMissing[appsv1.StatefulSet](t, fc, "operator-ns", shortName) expectMissing[corev1.Service](t, fc, "operator-ns", shortName) expectMissing[corev1.Secret](t, fc, "operator-ns", fullName) } func TestAnnotations(t *testing.T) { fc := fake.NewFakeClient() ft := &fakeTSClient{} zl, err := zap.NewDevelopment() if err != nil { t.Fatal(err) } sr := &ServiceReconciler{ Client: fc, ssr: &tailscaleSTSReconciler{ Client: fc, tsClient: ft, defaultTags: []string{"tag:k8s"}, operatorNamespace: "operator-ns", proxyImage: "tailscale/tailscale", }, logger: zl.Sugar(), } // Create a service that we should manage, and check that the initial round // of objects looks right. mustCreate(t, fc, &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test", Namespace: "default", // The apiserver is supposed to set the UID, but the fake client // doesn't. So, set it explicitly because other code later depends // on it being set. UID: types.UID("1234-UID"), Annotations: map[string]string{ "tailscale.com/expose": "true", }, }, Spec: corev1.ServiceSpec{ ClusterIP: "10.20.30.40", Type: corev1.ServiceTypeClusterIP, }, }) expectReconciled(t, sr, "default", "test") fullName, shortName := findGenName(t, fc, "default", "test", "svc") o := configOpts{ stsName: shortName, secretName: fullName, namespace: "default", parentType: "svc", hostname: "default-test", clusterTargetIP: "10.20.30.40", } expectEqual(t, fc, expectedSecret(t, o)) expectEqual(t, fc, expectedHeadlessService(shortName)) expectEqual(t, fc, expectedSTS(o)) want := &corev1.Service{ TypeMeta: metav1.TypeMeta{ Kind: "Service", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "test", Namespace: "default", Finalizers: []string{"tailscale.com/finalizer"}, UID: types.UID("1234-UID"), Annotations: map[string]string{ "tailscale.com/expose": "true", }, }, Spec: corev1.ServiceSpec{ ClusterIP: "10.20.30.40", Type: corev1.ServiceTypeClusterIP, }, } expectEqual(t, fc, want) // Turn the service back into a ClusterIP service, which should make the // operator clean up. mustUpdate(t, fc, "default", "test", func(s *corev1.Service) { delete(s.ObjectMeta.Annotations, "tailscale.com/expose") }) // synchronous StatefulSet deletion triggers a requeue. But, the StatefulSet // didn't create any child resources since this is all faked, so the // deletion goes through immediately. expectReconciled(t, sr, "default", "test") expectMissing[appsv1.StatefulSet](t, fc, "operator-ns", shortName) // Second time around, the rest of cleanup happens. expectReconciled(t, sr, "default", "test") expectMissing[appsv1.StatefulSet](t, fc, "operator-ns", shortName) expectMissing[corev1.Service](t, fc, "operator-ns", shortName) expectMissing[corev1.Secret](t, fc, "operator-ns", fullName) want = &corev1.Service{ TypeMeta: metav1.TypeMeta{ Kind: "Service", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "test", Namespace: "default", UID: types.UID("1234-UID"), }, Spec: corev1.ServiceSpec{ ClusterIP: "10.20.30.40", Type: corev1.ServiceTypeClusterIP, }, } expectEqual(t, fc, want) } func TestAnnotationIntoLB(t *testing.T) { fc := fake.NewFakeClient() ft := &fakeTSClient{} zl, err := zap.NewDevelopment() if err != nil { t.Fatal(err) } sr := &ServiceReconciler{ Client: fc, ssr: &tailscaleSTSReconciler{ Client: fc, tsClient: ft, defaultTags: []string{"tag:k8s"}, operatorNamespace: "operator-ns", proxyImage: "tailscale/tailscale", }, logger: zl.Sugar(), } // Create a service that we should manage, and check that the initial round // of objects looks right. mustCreate(t, fc, &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test", Namespace: "default", // The apiserver is supposed to set the UID, but the fake client // doesn't. So, set it explicitly because other code later depends // on it being set. UID: types.UID("1234-UID"), Annotations: map[string]string{ "tailscale.com/expose": "true", }, }, Spec: corev1.ServiceSpec{ ClusterIP: "10.20.30.40", Type: corev1.ServiceTypeClusterIP, }, }) expectReconciled(t, sr, "default", "test") fullName, shortName := findGenName(t, fc, "default", "test", "svc") o := configOpts{ stsName: shortName, secretName: fullName, namespace: "default", parentType: "svc", hostname: "default-test", clusterTargetIP: "10.20.30.40", } expectEqual(t, fc, expectedSecret(t, o)) expectEqual(t, fc, expectedHeadlessService(shortName)) expectEqual(t, fc, expectedSTS(o)) // Normally the Tailscale proxy pod would come up here and write its info // into the secret. Simulate that, since it would have normally happened at // this point and the LoadBalancer is going to expect this. mustUpdate(t, fc, "operator-ns", fullName, func(s *corev1.Secret) { if s.Data == nil { s.Data = map[string][]byte{} } s.Data["device_id"] = []byte("ts-id-1234") s.Data["device_fqdn"] = []byte("tailscale.device.name.") s.Data["device_ips"] = []byte(`["100.99.98.97", "2c0a:8083:94d4:2012:3165:34a5:3616:5fdf"]`) }) expectReconciled(t, sr, "default", "test") want := &corev1.Service{ TypeMeta: metav1.TypeMeta{ Kind: "Service", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "test", Namespace: "default", Finalizers: []string{"tailscale.com/finalizer"}, UID: types.UID("1234-UID"), Annotations: map[string]string{ "tailscale.com/expose": "true", }, }, Spec: corev1.ServiceSpec{ ClusterIP: "10.20.30.40", Type: corev1.ServiceTypeClusterIP, }, } expectEqual(t, fc, want) // Remove Tailscale's annotation, and at the same time convert the service // into a tailscale LoadBalancer. mustUpdate(t, fc, "default", "test", func(s *corev1.Service) { delete(s.ObjectMeta.Annotations, "tailscale.com/expose") s.Spec.Type = corev1.ServiceTypeLoadBalancer s.Spec.LoadBalancerClass = ptr.To("tailscale") }) expectReconciled(t, sr, "default", "test") // None of the proxy machinery should have changed... expectEqual(t, fc, expectedHeadlessService(shortName)) expectEqual(t, fc, expectedSTS(o)) // ... but the service should have a LoadBalancer status. want = &corev1.Service{ TypeMeta: metav1.TypeMeta{ Kind: "Service", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "test", Namespace: "default", Finalizers: []string{"tailscale.com/finalizer"}, UID: types.UID("1234-UID"), }, Spec: corev1.ServiceSpec{ ClusterIP: "10.20.30.40", Type: corev1.ServiceTypeLoadBalancer, LoadBalancerClass: ptr.To("tailscale"), }, Status: corev1.ServiceStatus{ LoadBalancer: corev1.LoadBalancerStatus{ Ingress: []corev1.LoadBalancerIngress{ { Hostname: "tailscale.device.name", }, { IP: "100.99.98.97", }, }, }, }, } expectEqual(t, fc, want) } func TestLBIntoAnnotation(t *testing.T) { fc := fake.NewFakeClient() ft := &fakeTSClient{} zl, err := zap.NewDevelopment() if err != nil { t.Fatal(err) } sr := &ServiceReconciler{ Client: fc, ssr: &tailscaleSTSReconciler{ Client: fc, tsClient: ft, defaultTags: []string{"tag:k8s"}, operatorNamespace: "operator-ns", proxyImage: "tailscale/tailscale", }, logger: zl.Sugar(), } // Create a service that we should manage, and check that the initial round // of objects looks right. mustCreate(t, fc, &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test", Namespace: "default", // The apiserver is supposed to set the UID, but the fake client // doesn't. So, set it explicitly because other code later depends // on it being set. UID: types.UID("1234-UID"), }, Spec: corev1.ServiceSpec{ ClusterIP: "10.20.30.40", Type: corev1.ServiceTypeLoadBalancer, LoadBalancerClass: ptr.To("tailscale"), }, }) expectReconciled(t, sr, "default", "test") fullName, shortName := findGenName(t, fc, "default", "test", "svc") o := configOpts{ stsName: shortName, secretName: fullName, namespace: "default", parentType: "svc", hostname: "default-test", clusterTargetIP: "10.20.30.40", } expectEqual(t, fc, expectedSecret(t, o)) expectEqual(t, fc, expectedHeadlessService(shortName)) expectEqual(t, fc, expectedSTS(o)) // Normally the Tailscale proxy pod would come up here and write its info // into the secret. Simulate that, then verify reconcile again and verify // that we get to the end. mustUpdate(t, fc, "operator-ns", fullName, func(s *corev1.Secret) { if s.Data == nil { s.Data = map[string][]byte{} } s.Data["device_id"] = []byte("ts-id-1234") s.Data["device_fqdn"] = []byte("tailscale.device.name.") s.Data["device_ips"] = []byte(`["100.99.98.97", "2c0a:8083:94d4:2012:3165:34a5:3616:5fdf"]`) }) expectReconciled(t, sr, "default", "test") want := &corev1.Service{ TypeMeta: metav1.TypeMeta{ Kind: "Service", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "test", Namespace: "default", Finalizers: []string{"tailscale.com/finalizer"}, UID: types.UID("1234-UID"), }, Spec: corev1.ServiceSpec{ ClusterIP: "10.20.30.40", Type: corev1.ServiceTypeLoadBalancer, LoadBalancerClass: ptr.To("tailscale"), }, Status: corev1.ServiceStatus{ LoadBalancer: corev1.LoadBalancerStatus{ Ingress: []corev1.LoadBalancerIngress{ { Hostname: "tailscale.device.name", }, { IP: "100.99.98.97", }, }, }, }, } expectEqual(t, fc, want) // Turn the service back into a ClusterIP service, but also add the // tailscale annotation. mustUpdate(t, fc, "default", "test", func(s *corev1.Service) { s.ObjectMeta.Annotations = map[string]string{ "tailscale.com/expose": "true", } s.Spec.Type = corev1.ServiceTypeClusterIP s.Spec.LoadBalancerClass = nil }) mustUpdateStatus(t, fc, "default", "test", func(s *corev1.Service) { // Fake client doesn't automatically delete the LoadBalancer status when // changing away from the LoadBalancer type, we have to do // controller-manager's work by hand. s.Status = corev1.ServiceStatus{} }) expectReconciled(t, sr, "default", "test") expectEqual(t, fc, expectedHeadlessService(shortName)) expectEqual(t, fc, expectedSTS(o)) want = &corev1.Service{ TypeMeta: metav1.TypeMeta{ Kind: "Service", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "test", Namespace: "default", Finalizers: []string{"tailscale.com/finalizer"}, Annotations: map[string]string{ "tailscale.com/expose": "true", }, UID: types.UID("1234-UID"), }, Spec: corev1.ServiceSpec{ ClusterIP: "10.20.30.40", Type: corev1.ServiceTypeClusterIP, }, } expectEqual(t, fc, want) } func TestCustomHostname(t *testing.T) { fc := fake.NewFakeClient() ft := &fakeTSClient{} zl, err := zap.NewDevelopment() if err != nil { t.Fatal(err) } sr := &ServiceReconciler{ Client: fc, ssr: &tailscaleSTSReconciler{ Client: fc, tsClient: ft, defaultTags: []string{"tag:k8s"}, operatorNamespace: "operator-ns", proxyImage: "tailscale/tailscale", }, logger: zl.Sugar(), } // Create a service that we should manage, and check that the initial round // of objects looks right. mustCreate(t, fc, &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test", Namespace: "default", // The apiserver is supposed to set the UID, but the fake client // doesn't. So, set it explicitly because other code later depends // on it being set. UID: types.UID("1234-UID"), Annotations: map[string]string{ "tailscale.com/expose": "true", "tailscale.com/hostname": "reindeer-flotilla", }, }, Spec: corev1.ServiceSpec{ ClusterIP: "10.20.30.40", Type: corev1.ServiceTypeClusterIP, }, }) expectReconciled(t, sr, "default", "test") fullName, shortName := findGenName(t, fc, "default", "test", "svc") o := configOpts{ stsName: shortName, secretName: fullName, namespace: "default", parentType: "svc", hostname: "reindeer-flotilla", clusterTargetIP: "10.20.30.40", } expectEqual(t, fc, expectedSecret(t, o)) expectEqual(t, fc, expectedHeadlessService(shortName)) expectEqual(t, fc, expectedSTS(o)) want := &corev1.Service{ TypeMeta: metav1.TypeMeta{ Kind: "Service", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "test", Namespace: "default", Finalizers: []string{"tailscale.com/finalizer"}, UID: types.UID("1234-UID"), Annotations: map[string]string{ "tailscale.com/expose": "true", "tailscale.com/hostname": "reindeer-flotilla", }, }, Spec: corev1.ServiceSpec{ ClusterIP: "10.20.30.40", Type: corev1.ServiceTypeClusterIP, }, } expectEqual(t, fc, want) // Turn the service back into a ClusterIP service, which should make the // operator clean up. mustUpdate(t, fc, "default", "test", func(s *corev1.Service) { delete(s.ObjectMeta.Annotations, "tailscale.com/expose") }) // synchronous StatefulSet deletion triggers a requeue. But, the StatefulSet // didn't create any child resources since this is all faked, so the // deletion goes through immediately. expectReconciled(t, sr, "default", "test") expectMissing[appsv1.StatefulSet](t, fc, "operator-ns", shortName) // Second time around, the rest of cleanup happens. expectReconciled(t, sr, "default", "test") expectMissing[appsv1.StatefulSet](t, fc, "operator-ns", shortName) expectMissing[corev1.Service](t, fc, "operator-ns", shortName) expectMissing[corev1.Secret](t, fc, "operator-ns", fullName) want = &corev1.Service{ TypeMeta: metav1.TypeMeta{ Kind: "Service", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "test", Namespace: "default", UID: types.UID("1234-UID"), Annotations: map[string]string{ "tailscale.com/hostname": "reindeer-flotilla", }, }, Spec: corev1.ServiceSpec{ ClusterIP: "10.20.30.40", Type: corev1.ServiceTypeClusterIP, }, } expectEqual(t, fc, want) } func TestCustomPriorityClassName(t *testing.T) { fc := fake.NewFakeClient() ft := &fakeTSClient{} zl, err := zap.NewDevelopment() if err != nil { t.Fatal(err) } sr := &ServiceReconciler{ Client: fc, ssr: &tailscaleSTSReconciler{ Client: fc, tsClient: ft, defaultTags: []string{"tag:k8s"}, operatorNamespace: "operator-ns", proxyImage: "tailscale/tailscale", proxyPriorityClassName: "custom-priority-class-name", }, logger: zl.Sugar(), } // Create a service that we should manage, and check that the initial round // of objects looks right. mustCreate(t, fc, &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test", Namespace: "default", // The apiserver is supposed to set the UID, but the fake client // doesn't. So, set it explicitly because other code later depends // on it being set. UID: types.UID("1234-UID"), Annotations: map[string]string{ "tailscale.com/expose": "true", "tailscale.com/hostname": "tailscale-critical", }, }, Spec: corev1.ServiceSpec{ ClusterIP: "10.20.30.40", Type: corev1.ServiceTypeClusterIP, }, }) expectReconciled(t, sr, "default", "test") fullName, shortName := findGenName(t, fc, "default", "test", "svc") o := configOpts{ stsName: shortName, secretName: fullName, namespace: "default", parentType: "svc", hostname: "tailscale-critical", priorityClassName: "custom-priority-class-name", clusterTargetIP: "10.20.30.40", } expectEqual(t, fc, expectedSTS(o)) } func TestDefaultLoadBalancer(t *testing.T) { fc := fake.NewFakeClient() ft := &fakeTSClient{} zl, err := zap.NewDevelopment() if err != nil { t.Fatal(err) } sr := &ServiceReconciler{ Client: fc, ssr: &tailscaleSTSReconciler{ Client: fc, tsClient: ft, defaultTags: []string{"tag:k8s"}, operatorNamespace: "operator-ns", proxyImage: "tailscale/tailscale", }, logger: zl.Sugar(), isDefaultLoadBalancer: true, } // Create a service that we should manage, and check that the initial round // of objects looks right. mustCreate(t, fc, &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test", Namespace: "default", // The apiserver is supposed to set the UID, but the fake client // doesn't. So, set it explicitly because other code later depends // on it being set. UID: types.UID("1234-UID"), }, Spec: corev1.ServiceSpec{ ClusterIP: "10.20.30.40", Type: corev1.ServiceTypeLoadBalancer, }, }) expectReconciled(t, sr, "default", "test") fullName, shortName := findGenName(t, fc, "default", "test", "svc") expectEqual(t, fc, expectedHeadlessService(shortName)) o := configOpts{ stsName: shortName, secretName: fullName, namespace: "default", parentType: "svc", hostname: "default-test", clusterTargetIP: "10.20.30.40", } expectEqual(t, fc, expectedSTS(o)) } func TestProxyFirewallMode(t *testing.T) { fc := fake.NewFakeClient() ft := &fakeTSClient{} zl, err := zap.NewDevelopment() if err != nil { t.Fatal(err) } sr := &ServiceReconciler{ Client: fc, ssr: &tailscaleSTSReconciler{ Client: fc, tsClient: ft, defaultTags: []string{"tag:k8s"}, operatorNamespace: "operator-ns", proxyImage: "tailscale/tailscale", tsFirewallMode: "nftables", }, logger: zl.Sugar(), isDefaultLoadBalancer: true, } // Create a service that we should manage, and check that the initial round // of objects looks right. mustCreate(t, fc, &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test", Namespace: "default", // The apiserver is supposed to set the UID, but the fake client // doesn't. So, set it explicitly because other code later depends // on it being set. UID: types.UID("1234-UID"), }, Spec: corev1.ServiceSpec{ ClusterIP: "10.20.30.40", Type: corev1.ServiceTypeLoadBalancer, }, }) expectReconciled(t, sr, "default", "test") fullName, shortName := findGenName(t, fc, "default", "test", "svc") o := configOpts{ stsName: shortName, secretName: fullName, namespace: "default", parentType: "svc", hostname: "default-test", firewallMode: "nftables", clusterTargetIP: "10.20.30.40", } expectEqual(t, fc, expectedSTS(o)) } func Test_isMagicDNSName(t *testing.T) { tests := []struct { in string want bool }{ { in: "foo.tail4567.ts.net", want: true, }, { in: "foo.tail4567.ts.net.", want: true, }, { in: "foo.tail4567", want: false, }, } for _, tt := range tests { t.Run(tt.in, func(t *testing.T) { if got := isMagicDNSName(tt.in); got != tt.want { t.Errorf("isMagicDNSName(%q) = %v, want %v", tt.in, got, tt.want) } }) } }