mirror of
https://github.com/tailscale/tailscale.git
synced 2024-12-01 22:15:51 +00:00
5bd19fd3e3
* cmd/k8s-operator,k8s-operator: introduce proxy configuration mechanism via ProxyClass custom resource. ProxyClass custom resource can be used to specify customizations for the proxy resources created by the operator. Add a reconciler that validates ProxyClass resources and sets a Ready condition to True or False with a corresponding reason and message. This is required because some fields (labels and annotations) require complex validations that cannot be performed at custom resource apply time. Reconcilers that use the ProxyClass to configure proxy resources are expected to verify that the ProxyClass is Ready and not proceed with resource creation if configuration from a ProxyClass that is not yet Ready is required. If a tailscale ingress/egress Service is annotated with a tailscale.com/proxy-class annotation, look up the corresponding ProxyClass and, if it is Ready, apply the configuration from the ProxyClass to the proxy's StatefulSet. If a tailscale Ingress has a tailscale.com/proxy-class annotation and the referenced ProxyClass custom resource is available and Ready, apply configuration from the ProxyClass to the proxy resources that will be created for the Ingress. Add a new .proxyClass field to the Connector spec. If connector.spec.proxyClass is set to a ProxyClass that is available and Ready, apply configuration from the ProxyClass to the proxy resources created for the Connector. Ensure that when Helm chart is packaged, the ProxyClass yaml is added to chart templates. Ensure that static manifest generator adds ProxyClass yaml to operator.yaml. Regenerate operator.yaml Signed-off-by: Irbe Krumina <irbe@tailscale.com>
1158 lines
34 KiB
Go
1158 lines
34 KiB
Go
// 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"
|
|
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
|
|
"tailscale.com/types/ptr"
|
|
"tailscale.com/util/mak"
|
|
)
|
|
|
|
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, "svc"))
|
|
expectEqual(t, fc, expectedSTS(t, fc, 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, "svc"))
|
|
expectEqual(t, fc, expectedSTS(t, fc, 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, "svc"))
|
|
expectEqual(t, fc, expectedSTS(t, fc, 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, "svc"))
|
|
expectEqual(t, fc, expectedSTS(t, fc, 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, "svc"))
|
|
expectEqual(t, fc, expectedSTS(t, fc, 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, "svc"))
|
|
expectEqual(t, fc, expectedSTS(t, fc, 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, "svc"))
|
|
expectEqual(t, fc, expectedSTS(t, fc, 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, "svc"))
|
|
expectEqual(t, fc, expectedSTS(t, fc, 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, "svc"))
|
|
expectEqual(t, fc, expectedSTS(t, fc, 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, "svc"))
|
|
expectEqual(t, fc, expectedSTS(t, fc, 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, "svc"))
|
|
expectEqual(t, fc, expectedSTS(t, fc, 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(t, fc, o))
|
|
}
|
|
|
|
func TestProxyClassForService(t *testing.T) {
|
|
// Setup
|
|
pc := &tsapi.ProxyClass{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "custom-metadata"},
|
|
Spec: tsapi.ProxyClassSpec{StatefulSet: &tsapi.StatefulSet{
|
|
Labels: map[string]string{"foo": "bar"},
|
|
Annotations: map[string]string{"bar.io/foo": "some-val"},
|
|
Pod: &tsapi.Pod{Annotations: map[string]string{"foo.io/bar": "some-val"}}}},
|
|
}
|
|
fc := fake.NewClientBuilder().
|
|
WithScheme(tsapi.GlobalScheme).
|
|
WithObjects(pc).
|
|
WithStatusSubresource(pc).
|
|
Build()
|
|
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(),
|
|
}
|
|
|
|
// 1. A new tailscale LoadBalancer Service is created without any
|
|
// ProxyClass. Resources get created for it as usual.
|
|
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, "svc"))
|
|
expectEqual(t, fc, expectedSTS(t, fc, opts))
|
|
|
|
// 2. The Service gets updated with tailscale.com/proxy-class label
|
|
// pointing at the 'custom-metadata' ProxyClass. The ProxyClass is not
|
|
// yet ready, so no changes are actually applied to the proxy resources.
|
|
mustUpdate(t, fc, "default", "test", func(svc *corev1.Service) {
|
|
mak.Set(&svc.Labels, LabelProxyClass, "custom-metadata")
|
|
})
|
|
expectReconciled(t, sr, "default", "test")
|
|
expectEqual(t, fc, expectedSTS(t, fc, opts))
|
|
|
|
// 3. ProxyClass is set to Ready, the Service gets reconciled by the
|
|
// services-reconciler and the customization from the ProxyClass is
|
|
// applied to the proxy resources.
|
|
mustUpdateStatus(t, fc, "", "custom-metadata", func(pc *tsapi.ProxyClass) {
|
|
pc.Status = tsapi.ProxyClassStatus{
|
|
Conditions: []tsapi.ConnectorCondition{{
|
|
Status: metav1.ConditionTrue,
|
|
Type: tsapi.ProxyClassready,
|
|
ObservedGeneration: pc.Generation,
|
|
}}}
|
|
})
|
|
opts.proxyClass = pc.Name
|
|
expectReconciled(t, sr, "default", "test")
|
|
expectEqual(t, fc, expectedSTS(t, fc, opts))
|
|
|
|
// 4. tailscale.com/proxy-class label is removed from the Service, the
|
|
// configuration from the ProxyClass is removed from the cluster
|
|
// resources.
|
|
mustUpdate(t, fc, "default", "test", func(svc *corev1.Service) {
|
|
delete(svc.Labels, LabelProxyClass)
|
|
})
|
|
opts.proxyClass = ""
|
|
expectReconciled(t, sr, "default", "test")
|
|
expectEqual(t, fc, expectedSTS(t, fc, opts))
|
|
}
|
|
|
|
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, "svc"))
|
|
o := configOpts{
|
|
stsName: shortName,
|
|
secretName: fullName,
|
|
namespace: "default",
|
|
parentType: "svc",
|
|
hostname: "default-test",
|
|
clusterTargetIP: "10.20.30.40",
|
|
}
|
|
expectEqual(t, fc, expectedSTS(t, fc, 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(t, fc, 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)
|
|
}
|
|
})
|
|
}
|
|
}
|