mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-26 11:35:35 +00:00
05093ea7d9
cmd/k8s-operator/deploy/crds,k8s-operator/apis/v1alpha1: allow to define an exit node via Connector CR. Make it possible to define an exit node to be deployed to a Kubernetes cluster via Connector Custom resource. Also changes to Connector API so that one Connector corresponds to one Tailnet node that can be either a subnet router or an exit node or both. The Kubernetes operator parses Connector custom resource and, if .spec.isExitNode is set, configures that Tailscale node deployed for that connector as an exit node. Signed-off-by: Irbe Krumina <irbe@tailscale.com> Co-authored-by: Anton Tolchanov <anton@tailscale.com>
1059 lines
30 KiB
Go
1059 lines
30 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"
|
|
"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)
|
|
}
|
|
})
|
|
}
|
|
}
|