@ -6,6 +6,7 @@
package main
import (
"context"
"testing"
"go.uber.org/zap"
@ -15,17 +16,18 @@ import (
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"tailscale.com/ipn"
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
"tailscale.com/kube/kubetypes"
"tailscale.com/tstest"
"tailscale.com/types/ptr"
"tailscale.com/util/mak"
)
func TestTailscaleIngress ( t * testing . T ) {
tsIngressClass := & networkingv1 . IngressClass { ObjectMeta : metav1 . ObjectMeta { Name : "tailscale" } , Spec : networkingv1 . IngressClassSpec { Controller : "tailscale.com/ts-ingress" } }
fc := fake . NewFakeClient ( tsIngressClass )
fc := fake . NewFakeClient ( ingressClass ( ) )
ft := & fakeTSClient { }
fakeTsnetServer := & fakeTSNetServer { certDomains : [ ] string { "foo.com" } }
zl , err := zap . NewDevelopment ( )
@ -46,45 +48,8 @@ func TestTailscaleIngress(t *testing.T) {
}
// 1. Resources get created for regular Ingress
ing := & networkingv1 . Ingress {
TypeMeta : metav1 . TypeMeta { Kind : "Ingress" , APIVersion : "networking.k8s.io/v1" } ,
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 : networkingv1 . IngressSpec {
IngressClassName : ptr . To ( "tailscale" ) ,
DefaultBackend : & networkingv1 . IngressBackend {
Service : & networkingv1 . IngressServiceBackend {
Name : "test" ,
Port : networkingv1 . ServiceBackendPort {
Number : 8080 ,
} ,
} ,
} ,
TLS : [ ] networkingv1 . IngressTLS {
{ Hosts : [ ] string { "default-test" } } ,
} ,
} ,
}
mustCreate ( t , fc , ing )
mustCreate ( t , fc , & corev1 . Service {
ObjectMeta : metav1 . ObjectMeta {
Name : "test" ,
Namespace : "default" ,
} ,
Spec : corev1 . ServiceSpec {
ClusterIP : "1.2.3.4" ,
Ports : [ ] corev1 . ServicePort { {
Port : 8080 ,
Name : "http" } ,
} ,
} ,
} )
mustCreate ( t , fc , ingress ( ) )
mustCreate ( t , fc , service ( ) )
expectReconciled ( t , ingR , "default" , "test" )
@ -114,6 +79,9 @@ func TestTailscaleIngress(t *testing.T) {
mak . Set ( & secret . Data , "device_fqdn" , [ ] byte ( "foo.tailnetxyz.ts.net" ) )
} )
expectReconciled ( t , ingR , "default" , "test" )
// Get the ingress and update it with expected changes
ing := ingress ( )
ing . Finalizers = append ( ing . Finalizers , "tailscale.com/finalizer" )
ing . Status . LoadBalancer = networkingv1 . IngressLoadBalancerStatus {
Ingress : [ ] networkingv1 . IngressLoadBalancerIngress {
@ -143,8 +111,7 @@ func TestTailscaleIngress(t *testing.T) {
}
func TestTailscaleIngressHostname ( t * testing . T ) {
tsIngressClass := & networkingv1 . IngressClass { ObjectMeta : metav1 . ObjectMeta { Name : "tailscale" } , Spec : networkingv1 . IngressClassSpec { Controller : "tailscale.com/ts-ingress" } }
fc := fake . NewFakeClient ( tsIngressClass )
fc := fake . NewFakeClient ( ingressClass ( ) )
ft := & fakeTSClient { }
fakeTsnetServer := & fakeTSNetServer { certDomains : [ ] string { "foo.com" } }
zl , err := zap . NewDevelopment ( )
@ -165,45 +132,8 @@ func TestTailscaleIngressHostname(t *testing.T) {
}
// 1. Resources get created for regular Ingress
ing := & networkingv1 . Ingress {
TypeMeta : metav1 . TypeMeta { Kind : "Ingress" , APIVersion : "networking.k8s.io/v1" } ,
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 : networkingv1 . IngressSpec {
IngressClassName : ptr . To ( "tailscale" ) ,
DefaultBackend : & networkingv1 . IngressBackend {
Service : & networkingv1 . IngressServiceBackend {
Name : "test" ,
Port : networkingv1 . ServiceBackendPort {
Number : 8080 ,
} ,
} ,
} ,
TLS : [ ] networkingv1 . IngressTLS {
{ Hosts : [ ] string { "default-test" } } ,
} ,
} ,
}
mustCreate ( t , fc , ing )
mustCreate ( t , fc , & corev1 . Service {
ObjectMeta : metav1 . ObjectMeta {
Name : "test" ,
Namespace : "default" ,
} ,
Spec : corev1 . ServiceSpec {
ClusterIP : "1.2.3.4" ,
Ports : [ ] corev1 . ServicePort { {
Port : 8080 ,
Name : "http" } ,
} ,
} ,
} )
mustCreate ( t , fc , ingress ( ) )
mustCreate ( t , fc , service ( ) )
expectReconciled ( t , ingR , "default" , "test" )
@ -241,8 +171,10 @@ func TestTailscaleIngressHostname(t *testing.T) {
mak . Set ( & secret . Data , "device_fqdn" , [ ] byte ( "foo.tailnetxyz.ts.net" ) )
} )
expectReconciled ( t , ingR , "default" , "test" )
ing . Finalizers = append ( ing . Finalizers , "tailscale.com/finalizer" )
// Get the ingress and update it with expected changes
ing := ingress ( )
ing . Finalizers = append ( ing . Finalizers , "tailscale.com/finalizer" )
expectEqual ( t , fc , ing )
// 3. Ingress proxy with capability version >= 110 advertises HTTPS endpoint
@ -299,10 +231,9 @@ func TestTailscaleIngressWithProxyClass(t *testing.T) {
Annotations : map [ string ] string { "bar.io/foo" : "some-val" } ,
Pod : & tsapi . Pod { Annotations : map [ string ] string { "foo.io/bar" : "some-val" } } } } ,
}
tsIngressClass := & networkingv1 . IngressClass { ObjectMeta : metav1 . ObjectMeta { Name : "tailscale" } , Spec : networkingv1 . IngressClassSpec { Controller : "tailscale.com/ts-ingress" } }
fc := fake . NewClientBuilder ( ) .
WithScheme ( tsapi . GlobalScheme ) .
WithObjects ( pc , tsIngressClass ) .
WithObjects ( pc , ingressClass ( ) ) .
WithStatusSubresource ( pc ) .
Build ( )
ft := & fakeTSClient { }
@ -326,45 +257,8 @@ func TestTailscaleIngressWithProxyClass(t *testing.T) {
// 1. Ingress is created with no ProxyClass specified, default proxy
// resources get configured.
ing := & networkingv1 . Ingress {
TypeMeta : metav1 . TypeMeta { Kind : "Ingress" , APIVersion : "networking.k8s.io/v1" } ,
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 : networkingv1 . IngressSpec {
IngressClassName : ptr . To ( "tailscale" ) ,
DefaultBackend : & networkingv1 . IngressBackend {
Service : & networkingv1 . IngressServiceBackend {
Name : "test" ,
Port : networkingv1 . ServiceBackendPort {
Number : 8080 ,
} ,
} ,
} ,
TLS : [ ] networkingv1 . IngressTLS {
{ Hosts : [ ] string { "default-test" } } ,
} ,
} ,
}
mustCreate ( t , fc , ing )
mustCreate ( t , fc , & corev1 . Service {
ObjectMeta : metav1 . ObjectMeta {
Name : "test" ,
Namespace : "default" ,
} ,
Spec : corev1 . ServiceSpec {
ClusterIP : "1.2.3.4" ,
Ports : [ ] corev1 . ServicePort { {
Port : 8080 ,
Name : "http" } ,
} ,
} ,
} )
mustCreate ( t , fc , ingress ( ) )
mustCreate ( t , fc , service ( ) )
expectReconciled ( t , ingR , "default" , "test" )
@ -432,54 +326,19 @@ func TestTailscaleIngressWithServiceMonitor(t *testing.T) {
ObservedGeneration : 1 ,
} } } ,
}
ing := & networkingv1 . Ingress {
TypeMeta : metav1 . TypeMeta { Kind : "Ingress" , APIVersion : "networking.k8s.io/v1" } ,
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" ) ,
Labels : map [ string ] string {
"tailscale.com/proxy-class" : "metrics" ,
} ,
} ,
Spec : networkingv1 . IngressSpec {
IngressClassName : ptr . To ( "tailscale" ) ,
DefaultBackend : & networkingv1 . IngressBackend {
Service : & networkingv1 . IngressServiceBackend {
Name : "test" ,
Port : networkingv1 . ServiceBackendPort {
Number : 8080 ,
} ,
} ,
} ,
TLS : [ ] networkingv1 . IngressTLS {
{ Hosts : [ ] string { "default-test" } } ,
} ,
} ,
}
svc := & corev1 . Service {
ObjectMeta : metav1 . ObjectMeta {
Name : "test" ,
Namespace : "default" ,
} ,
Spec : corev1 . ServiceSpec {
ClusterIP : "1.2.3.4" ,
Ports : [ ] corev1 . ServicePort { {
Port : 8080 ,
Name : "http" } ,
} ,
} ,
}
crd := & apiextensionsv1 . CustomResourceDefinition { ObjectMeta : metav1 . ObjectMeta { Name : serviceMonitorCRD } }
tsIngressClass := & networkingv1 . IngressClass { ObjectMeta : metav1 . ObjectMeta { Name : "tailscale" } , Spec : networkingv1 . IngressClassSpec { Controller : "tailscale.com/ts-ingress" } }
// Create fake client with ProxyClass, IngressClass, Ingress with metrics ProxyClass, and Service
ing := ingress ( )
ing . Labels = map [ string ] string {
LabelProxyClass : "metrics" ,
}
fc := fake . NewClientBuilder ( ) .
WithScheme ( tsapi . GlobalScheme ) .
WithObjects ( pc , tsIngressClass , ing , svc ) .
WithObjects ( pc , ingressClass ( ) , ing , service ( ) ) .
WithStatusSubresource ( pc ) .
Build ( )
ft := & fakeTSClient { }
fakeTsnetServer := & fakeTSNetServer { certDomains : [ ] string { "foo.com" } }
zl , err := zap . NewDevelopment ( )
@ -560,3 +419,118 @@ func TestTailscaleIngressWithServiceMonitor(t *testing.T) {
expectMissing [ corev1 . Service ] ( t , fc , "operator-ns" , metricsResourceName ( shortName ) )
// ServiceMonitor gets garbage collected when the Service is deleted - we cannot test that here.
}
func TestIngressLetsEncryptStaging ( t * testing . T ) {
cl := tstest . NewClock ( tstest . ClockOpts { } )
zl := zap . Must ( zap . NewDevelopment ( ) )
pcLEStaging , pcLEStagingFalse , pcOther := proxyClassesForLEStagingTest ( )
testCases := testCasesForLEStagingTests ( pcLEStaging , pcLEStagingFalse , pcOther )
for _ , tt := range testCases {
t . Run ( tt . name , func ( t * testing . T ) {
builder := fake . NewClientBuilder ( ) .
WithScheme ( tsapi . GlobalScheme )
builder = builder . WithObjects ( pcLEStaging , pcLEStagingFalse , pcOther ) .
WithStatusSubresource ( pcLEStaging , pcLEStagingFalse , pcOther )
fc := builder . Build ( )
if tt . proxyClassPerResource != "" || tt . defaultProxyClass != "" {
name := tt . proxyClassPerResource
if name == "" {
name = tt . defaultProxyClass
}
setProxyClassReady ( t , fc , cl , name )
}
mustCreate ( t , fc , ingressClass ( ) )
mustCreate ( t , fc , service ( ) )
ing := ingress ( )
if tt . proxyClassPerResource != "" {
ing . Labels = map [ string ] string {
LabelProxyClass : tt . proxyClassPerResource ,
}
}
mustCreate ( t , fc , ing )
ingR := & IngressReconciler {
Client : fc ,
ssr : & tailscaleSTSReconciler {
Client : fc ,
tsClient : & fakeTSClient { } ,
tsnetServer : & fakeTSNetServer { certDomains : [ ] string { "test-host" } } ,
defaultTags : [ ] string { "tag:test" } ,
operatorNamespace : "operator-ns" ,
proxyImage : "tailscale/tailscale:test" ,
} ,
logger : zl . Sugar ( ) ,
defaultProxyClass : tt . defaultProxyClass ,
}
expectReconciled ( t , ingR , "default" , "test" )
_ , shortName := findGenName ( t , fc , "default" , "test" , "ingress" )
sts := & appsv1 . StatefulSet { }
if err := fc . Get ( context . Background ( ) , client . ObjectKey { Namespace : "operator-ns" , Name : shortName } , sts ) ; err != nil {
t . Fatalf ( "failed to get StatefulSet: %v" , err )
}
if tt . useLEStagingEndpoint {
verifyEnvVar ( t , sts , "TS_DEBUG_ACME_DIRECTORY_URL" , letsEncryptStagingEndpoint )
} else {
verifyEnvVarNotPresent ( t , sts , "TS_DEBUG_ACME_DIRECTORY_URL" )
}
} )
}
}
func ingressClass ( ) * networkingv1 . IngressClass {
return & networkingv1 . IngressClass {
ObjectMeta : metav1 . ObjectMeta { Name : "tailscale" } ,
Spec : networkingv1 . IngressClassSpec { Controller : "tailscale.com/ts-ingress" } ,
}
}
func service ( ) * corev1 . Service {
return & corev1 . Service {
ObjectMeta : metav1 . ObjectMeta {
Name : "test" ,
Namespace : "default" ,
} ,
Spec : corev1 . ServiceSpec {
ClusterIP : "1.2.3.4" ,
Ports : [ ] corev1 . ServicePort { {
Port : 8080 ,
Name : "http" } ,
} ,
} ,
}
}
func ingress ( ) * networkingv1 . Ingress {
return & networkingv1 . Ingress {
TypeMeta : metav1 . TypeMeta { Kind : "Ingress" , APIVersion : "networking.k8s.io/v1" } ,
ObjectMeta : metav1 . ObjectMeta {
Name : "test" ,
Namespace : "default" ,
UID : types . UID ( "1234-UID" ) ,
} ,
Spec : networkingv1 . IngressSpec {
IngressClassName : ptr . To ( "tailscale" ) ,
DefaultBackend : & networkingv1 . IngressBackend {
Service : & networkingv1 . IngressServiceBackend {
Name : "test" ,
Port : networkingv1 . ServiceBackendPort {
Number : 8080 ,
} ,
} ,
} ,
TLS : [ ] networkingv1 . IngressTLS {
{ Hosts : [ ] string { "default-test" } } ,
} ,
} ,
}
}