2024-02-08 06:45:42 +00:00
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !plan9
package main
import (
2025-03-21 08:53:41 +00:00
"context"
2024-02-08 06:45:42 +00:00
"testing"
"go.uber.org/zap"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
2024-12-03 12:35:25 +00:00
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
2024-02-08 06:45:42 +00:00
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
2025-04-17 16:14:34 +01:00
"k8s.io/client-go/tools/record"
2025-03-21 08:53:41 +00:00
"sigs.k8s.io/controller-runtime/pkg/client"
2024-02-08 06:45:42 +00:00
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"tailscale.com/ipn"
2024-02-13 05:27:54 +00:00
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
2024-09-08 22:57:29 +03:00
"tailscale.com/kube/kubetypes"
2025-03-21 08:53:41 +00:00
"tailscale.com/tstest"
2024-02-08 06:45:42 +00:00
"tailscale.com/types/ptr"
"tailscale.com/util/mak"
)
func TestTailscaleIngress ( t * testing . T ) {
2025-03-21 08:53:41 +00:00
fc := fake . NewFakeClient ( ingressClass ( ) )
2024-02-08 06:45:42 +00:00
ft := & fakeTSClient { }
fakeTsnetServer := & fakeTSNetServer { certDomains : [ ] string { "foo.com" } }
zl , err := zap . NewDevelopment ( )
if err != nil {
t . Fatal ( err )
}
ingR := & IngressReconciler {
Client : fc ,
ssr : & tailscaleSTSReconciler {
Client : fc ,
tsClient : ft ,
tsnetServer : fakeTsnetServer ,
defaultTags : [ ] string { "tag:k8s" } ,
operatorNamespace : "operator-ns" ,
proxyImage : "tailscale/tailscale" ,
} ,
logger : zl . Sugar ( ) ,
}
// 1. Resources get created for regular Ingress
2025-03-21 08:53:41 +00:00
mustCreate ( t , fc , ingress ( ) )
mustCreate ( t , fc , service ( ) )
2024-02-08 06:45:42 +00:00
expectReconciled ( t , ingR , "default" , "test" )
fullName , shortName := findGenName ( t , fc , "default" , "test" , "ingress" )
opts := configOpts {
2024-03-19 14:54:17 +00:00
stsName : shortName ,
secretName : fullName ,
namespace : "default" ,
parentType : "ingress" ,
hostname : "default-test" ,
2024-09-08 21:06:07 +03:00
app : kubetypes . AppIngressResource ,
2024-02-08 06:45:42 +00:00
}
serveConfig := & ipn . ServeConfig {
TCP : map [ uint16 ] * ipn . TCPPortHandler { 443 : { HTTPS : true } } ,
Web : map [ ipn . HostPort ] * ipn . WebServerConfig { "${TS_CERT_DOMAIN}:443" : { Handlers : map [ string ] * ipn . HTTPHandler { "/" : { Proxy : "http://1.2.3.4:8080/" } } } } ,
}
opts . serveConfig = serveConfig
2025-01-17 05:37:53 +00:00
expectEqual ( t , fc , expectedSecret ( t , fc , opts ) )
expectEqual ( t , fc , expectedHeadlessService ( shortName , "ingress" ) )
expectEqual ( t , fc , expectedSTSUserspace ( t , fc , opts ) , removeHashAnnotation , removeResourceReqs )
2024-02-08 06:45:42 +00:00
// 2. Ingress status gets updated with ingress proxy's MagicDNS name
// once that becomes available.
mustUpdate ( t , fc , "operator-ns" , opts . secretName , func ( secret * corev1 . Secret ) {
mak . Set ( & secret . Data , "device_id" , [ ] byte ( "1234" ) )
mak . Set ( & secret . Data , "device_fqdn" , [ ] byte ( "foo.tailnetxyz.ts.net" ) )
} )
expectReconciled ( t , ingR , "default" , "test" )
2025-03-21 08:53:41 +00:00
// Get the ingress and update it with expected changes
ing := ingress ( )
2024-02-08 06:45:42 +00:00
ing . Finalizers = append ( ing . Finalizers , "tailscale.com/finalizer" )
ing . Status . LoadBalancer = networkingv1 . IngressLoadBalancerStatus {
Ingress : [ ] networkingv1 . IngressLoadBalancerIngress {
{ Hostname : "foo.tailnetxyz.ts.net" , Ports : [ ] networkingv1 . IngressPortStatus { { Port : 443 , Protocol : "TCP" } } } ,
} ,
}
2025-01-17 05:37:53 +00:00
expectEqual ( t , fc , ing )
2024-02-08 06:45:42 +00:00
// 3. Resources get created for Ingress that should allow forwarding
// cluster traffic
mustUpdate ( t , fc , "default" , "test" , func ( ing * networkingv1 . Ingress ) {
mak . Set ( & ing . ObjectMeta . Annotations , AnnotationExperimentalForwardClusterTrafficViaL7IngresProxy , "true" )
} )
opts . shouldEnableForwardingClusterTrafficViaIngress = true
expectReconciled ( t , ingR , "default" , "test" )
2025-01-17 05:37:53 +00:00
expectEqual ( t , fc , expectedSTS ( t , fc , opts ) , removeHashAnnotation , removeResourceReqs )
2024-02-08 06:45:42 +00:00
// 4. Resources get cleaned up when Ingress class is unset
mustUpdate ( t , fc , "default" , "test" , func ( ing * networkingv1 . Ingress ) {
ing . Spec . IngressClassName = ptr . To ( "nginx" )
} )
expectReconciled ( t , ingR , "default" , "test" )
expectReconciled ( t , ingR , "default" , "test" ) // deleting Ingress STS requires two reconciles
expectMissing [ appsv1 . StatefulSet ] ( t , fc , "operator-ns" , shortName )
expectMissing [ corev1 . Service ] ( t , fc , "operator-ns" , shortName )
expectMissing [ corev1 . Secret ] ( t , fc , "operator-ns" , fullName )
}
2024-02-13 05:27:54 +00:00
2024-12-04 12:00:04 +00:00
func TestTailscaleIngressHostname ( t * testing . T ) {
2025-03-21 08:53:41 +00:00
fc := fake . NewFakeClient ( ingressClass ( ) )
2024-12-04 12:00:04 +00:00
ft := & fakeTSClient { }
fakeTsnetServer := & fakeTSNetServer { certDomains : [ ] string { "foo.com" } }
zl , err := zap . NewDevelopment ( )
if err != nil {
t . Fatal ( err )
}
ingR := & IngressReconciler {
Client : fc ,
ssr : & tailscaleSTSReconciler {
Client : fc ,
tsClient : ft ,
tsnetServer : fakeTsnetServer ,
defaultTags : [ ] string { "tag:k8s" } ,
operatorNamespace : "operator-ns" ,
proxyImage : "tailscale/tailscale" ,
} ,
logger : zl . Sugar ( ) ,
}
// 1. Resources get created for regular Ingress
2025-03-21 08:53:41 +00:00
mustCreate ( t , fc , ingress ( ) )
mustCreate ( t , fc , service ( ) )
2024-12-04 12:00:04 +00:00
expectReconciled ( t , ingR , "default" , "test" )
fullName , shortName := findGenName ( t , fc , "default" , "test" , "ingress" )
mustCreate ( t , fc , & corev1 . Pod {
ObjectMeta : metav1 . ObjectMeta {
Name : fullName ,
Namespace : "operator-ns" ,
UID : "test-uid" ,
} ,
} )
opts := configOpts {
stsName : shortName ,
secretName : fullName ,
namespace : "default" ,
parentType : "ingress" ,
hostname : "default-test" ,
app : kubetypes . AppIngressResource ,
}
serveConfig := & ipn . ServeConfig {
TCP : map [ uint16 ] * ipn . TCPPortHandler { 443 : { HTTPS : true } } ,
Web : map [ ipn . HostPort ] * ipn . WebServerConfig { "${TS_CERT_DOMAIN}:443" : { Handlers : map [ string ] * ipn . HTTPHandler { "/" : { Proxy : "http://1.2.3.4:8080/" } } } } ,
}
opts . serveConfig = serveConfig
2025-01-17 05:37:53 +00:00
expectEqual ( t , fc , expectedSecret ( t , fc , opts ) )
expectEqual ( t , fc , expectedHeadlessService ( shortName , "ingress" ) )
expectEqual ( t , fc , expectedSTSUserspace ( t , fc , opts ) , removeHashAnnotation , removeResourceReqs )
2024-12-04 12:00:04 +00:00
// 2. Ingress proxy with capability version >= 110 does not have an HTTPS endpoint set
mustUpdate ( t , fc , "operator-ns" , opts . secretName , func ( secret * corev1 . Secret ) {
mak . Set ( & secret . Data , "device_id" , [ ] byte ( "1234" ) )
mak . Set ( & secret . Data , "tailscale_capver" , [ ] byte ( "110" ) )
mak . Set ( & secret . Data , "pod_uid" , [ ] byte ( "test-uid" ) )
mak . Set ( & secret . Data , "device_fqdn" , [ ] byte ( "foo.tailnetxyz.ts.net" ) )
} )
expectReconciled ( t , ingR , "default" , "test" )
2025-03-21 08:53:41 +00:00
// Get the ingress and update it with expected changes
ing := ingress ( )
ing . Finalizers = append ( ing . Finalizers , "tailscale.com/finalizer" )
2025-01-17 05:37:53 +00:00
expectEqual ( t , fc , ing )
2024-12-04 12:00:04 +00:00
// 3. Ingress proxy with capability version >= 110 advertises HTTPS endpoint
mustUpdate ( t , fc , "operator-ns" , opts . secretName , func ( secret * corev1 . Secret ) {
mak . Set ( & secret . Data , "device_id" , [ ] byte ( "1234" ) )
mak . Set ( & secret . Data , "tailscale_capver" , [ ] byte ( "110" ) )
mak . Set ( & secret . Data , "pod_uid" , [ ] byte ( "test-uid" ) )
mak . Set ( & secret . Data , "device_fqdn" , [ ] byte ( "foo.tailnetxyz.ts.net" ) )
mak . Set ( & secret . Data , "https_endpoint" , [ ] byte ( "foo.tailnetxyz.ts.net" ) )
} )
expectReconciled ( t , ingR , "default" , "test" )
ing . Status . LoadBalancer = networkingv1 . IngressLoadBalancerStatus {
Ingress : [ ] networkingv1 . IngressLoadBalancerIngress {
{ Hostname : "foo.tailnetxyz.ts.net" , Ports : [ ] networkingv1 . IngressPortStatus { { Port : 443 , Protocol : "TCP" } } } ,
} ,
}
2025-01-17 05:37:53 +00:00
expectEqual ( t , fc , ing )
2024-12-04 12:00:04 +00:00
// 4. Ingress proxy with capability version >= 110 does not have an HTTPS endpoint ready
mustUpdate ( t , fc , "operator-ns" , opts . secretName , func ( secret * corev1 . Secret ) {
mak . Set ( & secret . Data , "device_id" , [ ] byte ( "1234" ) )
mak . Set ( & secret . Data , "tailscale_capver" , [ ] byte ( "110" ) )
mak . Set ( & secret . Data , "pod_uid" , [ ] byte ( "test-uid" ) )
mak . Set ( & secret . Data , "device_fqdn" , [ ] byte ( "foo.tailnetxyz.ts.net" ) )
mak . Set ( & secret . Data , "https_endpoint" , [ ] byte ( "no-https" ) )
} )
expectReconciled ( t , ingR , "default" , "test" )
ing . Status . LoadBalancer . Ingress = nil
2025-01-17 05:37:53 +00:00
expectEqual ( t , fc , ing )
2024-12-04 12:00:04 +00:00
// 5. Ingress proxy's state has https_endpoints set, but its capver is not matching Pod UID (downgrade)
mustUpdate ( t , fc , "operator-ns" , opts . secretName , func ( secret * corev1 . Secret ) {
mak . Set ( & secret . Data , "device_id" , [ ] byte ( "1234" ) )
mak . Set ( & secret . Data , "tailscale_capver" , [ ] byte ( "110" ) )
mak . Set ( & secret . Data , "pod_uid" , [ ] byte ( "not-the-right-uid" ) )
mak . Set ( & secret . Data , "device_fqdn" , [ ] byte ( "foo.tailnetxyz.ts.net" ) )
mak . Set ( & secret . Data , "https_endpoint" , [ ] byte ( "bar.tailnetxyz.ts.net" ) )
} )
ing . Status . LoadBalancer = networkingv1 . IngressLoadBalancerStatus {
Ingress : [ ] networkingv1 . IngressLoadBalancerIngress {
{ Hostname : "foo.tailnetxyz.ts.net" , Ports : [ ] networkingv1 . IngressPortStatus { { Port : 443 , Protocol : "TCP" } } } ,
} ,
}
expectReconciled ( t , ingR , "default" , "test" )
2025-01-17 05:37:53 +00:00
expectEqual ( t , fc , ing )
2024-12-04 12:00:04 +00:00
}
2024-02-13 05:27:54 +00:00
func TestTailscaleIngressWithProxyClass ( t * testing . T ) {
// Setup
pc := & tsapi . ProxyClass {
ObjectMeta : metav1 . ObjectMeta { Name : "custom-metadata" } ,
Spec : tsapi . ProxyClassSpec { StatefulSet : & tsapi . StatefulSet {
2025-01-09 07:15:19 +00:00
Labels : tsapi . Labels { "foo" : "bar" } ,
2024-02-13 05:27:54 +00:00
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 ) .
2025-03-21 08:53:41 +00:00
WithObjects ( pc , ingressClass ( ) ) .
2024-02-13 05:27:54 +00:00
WithStatusSubresource ( pc ) .
Build ( )
ft := & fakeTSClient { }
fakeTsnetServer := & fakeTSNetServer { certDomains : [ ] string { "foo.com" } }
zl , err := zap . NewDevelopment ( )
if err != nil {
t . Fatal ( err )
}
ingR := & IngressReconciler {
Client : fc ,
ssr : & tailscaleSTSReconciler {
Client : fc ,
tsClient : ft ,
tsnetServer : fakeTsnetServer ,
defaultTags : [ ] string { "tag:k8s" } ,
operatorNamespace : "operator-ns" ,
proxyImage : "tailscale/tailscale" ,
} ,
logger : zl . Sugar ( ) ,
}
// 1. Ingress is created with no ProxyClass specified, default proxy
// resources get configured.
2025-03-21 08:53:41 +00:00
mustCreate ( t , fc , ingress ( ) )
mustCreate ( t , fc , service ( ) )
2024-02-13 05:27:54 +00:00
expectReconciled ( t , ingR , "default" , "test" )
fullName , shortName := findGenName ( t , fc , "default" , "test" , "ingress" )
opts := configOpts {
2024-03-19 14:54:17 +00:00
stsName : shortName ,
secretName : fullName ,
namespace : "default" ,
parentType : "ingress" ,
hostname : "default-test" ,
2024-09-08 21:06:07 +03:00
app : kubetypes . AppIngressResource ,
2024-02-13 05:27:54 +00:00
}
serveConfig := & ipn . ServeConfig {
TCP : map [ uint16 ] * ipn . TCPPortHandler { 443 : { HTTPS : true } } ,
Web : map [ ipn . HostPort ] * ipn . WebServerConfig { "${TS_CERT_DOMAIN}:443" : { Handlers : map [ string ] * ipn . HTTPHandler { "/" : { Proxy : "http://1.2.3.4:8080/" } } } } ,
}
opts . serveConfig = serveConfig
2025-01-17 05:37:53 +00:00
expectEqual ( t , fc , expectedSecret ( t , fc , opts ) )
expectEqual ( t , fc , expectedHeadlessService ( shortName , "ingress" ) )
expectEqual ( t , fc , expectedSTSUserspace ( t , fc , opts ) , removeHashAnnotation , removeResourceReqs )
2024-02-13 05:27:54 +00:00
// 2. Ingress is updated to specify a ProxyClass, ProxyClass is not yet
// ready, so proxy resource configuration does not change.
mustUpdate ( t , fc , "default" , "test" , func ( ing * networkingv1 . Ingress ) {
mak . Set ( & ing . ObjectMeta . Labels , LabelProxyClass , "custom-metadata" )
} )
expectReconciled ( t , ingR , "default" , "test" )
2025-01-17 05:37:53 +00:00
expectEqual ( t , fc , expectedSTSUserspace ( t , fc , opts ) , removeHashAnnotation , removeResourceReqs )
2024-02-13 05:27:54 +00:00
// 3. ProxyClass is set to Ready by proxy-class reconciler. Ingress get
// reconciled and configuration from the ProxyClass is applied to the
// created proxy resources.
mustUpdateStatus ( t , fc , "" , "custom-metadata" , func ( pc * tsapi . ProxyClass ) {
pc . Status = tsapi . ProxyClassStatus {
2024-06-18 19:01:40 +01:00
Conditions : [ ] metav1 . Condition { {
2024-02-13 05:27:54 +00:00
Status : metav1 . ConditionTrue ,
2024-10-08 17:34:34 +01:00
Type : string ( tsapi . ProxyClassReady ) ,
2024-02-13 05:27:54 +00:00
ObservedGeneration : pc . Generation ,
} } }
} )
expectReconciled ( t , ingR , "default" , "test" )
opts . proxyClass = pc . Name
2025-01-17 05:37:53 +00:00
expectEqual ( t , fc , expectedSTSUserspace ( t , fc , opts ) , removeHashAnnotation , removeResourceReqs )
2024-02-13 05:27:54 +00:00
// 4. tailscale.com/proxy-class label is removed from the Ingress, the
// Ingress gets reconciled and the custom ProxyClass configuration is
// removed from the proxy resources.
mustUpdate ( t , fc , "default" , "test" , func ( ing * networkingv1 . Ingress ) {
delete ( ing . ObjectMeta . Labels , LabelProxyClass )
} )
expectReconciled ( t , ingR , "default" , "test" )
opts . proxyClass = ""
2025-01-17 05:37:53 +00:00
expectEqual ( t , fc , expectedSTSUserspace ( t , fc , opts ) , removeHashAnnotation , removeResourceReqs )
2024-02-13 05:27:54 +00:00
}
2024-12-03 12:35:25 +00:00
func TestTailscaleIngressWithServiceMonitor ( t * testing . T ) {
pc := & tsapi . ProxyClass {
ObjectMeta : metav1 . ObjectMeta { Name : "metrics" , Generation : 1 } ,
2025-01-09 07:15:19 +00:00
Spec : tsapi . ProxyClassSpec { } ,
2024-12-03 12:35:25 +00:00
Status : tsapi . ProxyClassStatus {
Conditions : [ ] metav1 . Condition { {
Status : metav1 . ConditionTrue ,
Type : string ( tsapi . ProxyClassReady ) ,
ObservedGeneration : 1 ,
} } } ,
}
2025-01-09 07:15:19 +00:00
crd := & apiextensionsv1 . CustomResourceDefinition { ObjectMeta : metav1 . ObjectMeta { Name : serviceMonitorCRD } }
2025-03-21 08:53:41 +00:00
// Create fake client with ProxyClass, IngressClass, Ingress with metrics ProxyClass, and Service
ing := ingress ( )
ing . Labels = map [ string ] string {
LabelProxyClass : "metrics" ,
}
2025-01-09 07:15:19 +00:00
fc := fake . NewClientBuilder ( ) .
WithScheme ( tsapi . GlobalScheme ) .
2025-03-21 08:53:41 +00:00
WithObjects ( pc , ingressClass ( ) , ing , service ( ) ) .
2025-01-09 07:15:19 +00:00
WithStatusSubresource ( pc ) .
Build ( )
2025-03-21 08:53:41 +00:00
2025-01-09 07:15:19 +00:00
ft := & fakeTSClient { }
fakeTsnetServer := & fakeTSNetServer { certDomains : [ ] string { "foo.com" } }
zl , err := zap . NewDevelopment ( )
if err != nil {
t . Fatal ( err )
}
ingR := & IngressReconciler {
Client : fc ,
ssr : & tailscaleSTSReconciler {
Client : fc ,
tsClient : ft ,
tsnetServer : fakeTsnetServer ,
defaultTags : [ ] string { "tag:k8s" } ,
operatorNamespace : "operator-ns" ,
proxyImage : "tailscale/tailscale" ,
} ,
logger : zl . Sugar ( ) ,
}
2024-12-03 12:35:25 +00:00
expectReconciled ( t , ingR , "default" , "test" )
fullName , shortName := findGenName ( t , fc , "default" , "test" , "ingress" )
2025-01-09 07:15:19 +00:00
serveConfig := & ipn . ServeConfig {
TCP : map [ uint16 ] * ipn . TCPPortHandler { 443 : { HTTPS : true } } ,
Web : map [ ipn . HostPort ] * ipn . WebServerConfig { "${TS_CERT_DOMAIN}:443" : { Handlers : map [ string ] * ipn . HTTPHandler { "/" : { Proxy : "http://1.2.3.4:8080/" } } } } ,
}
2024-12-03 12:35:25 +00:00
opts := configOpts {
stsName : shortName ,
secretName : fullName ,
namespace : "default" ,
tailscaleNamespace : "operator-ns" ,
parentType : "ingress" ,
hostname : "default-test" ,
app : kubetypes . AppIngressResource ,
namespaced : true ,
proxyType : proxyTypeIngressResource ,
2025-01-09 07:15:19 +00:00
serveConfig : serveConfig ,
resourceVersion : "1" ,
2024-12-03 12:35:25 +00:00
}
2025-01-09 07:15:19 +00:00
// 1. Enable metrics- expect metrics Service to be created
mustUpdate ( t , fc , "" , "metrics" , func ( proxyClass * tsapi . ProxyClass ) {
proxyClass . Spec . Metrics = & tsapi . Metrics { Enable : true }
} )
opts . enableMetrics = true
expectReconciled ( t , ingR , "default" , "test" )
2025-01-17 05:37:53 +00:00
expectEqual ( t , fc , expectedMetricsService ( opts ) )
2025-01-09 07:15:19 +00:00
2024-12-03 12:35:25 +00:00
// 2. Enable ServiceMonitor - should not error when there is no ServiceMonitor CRD in cluster
mustUpdate ( t , fc , "" , "metrics" , func ( pc * tsapi . ProxyClass ) {
2025-01-09 07:15:19 +00:00
pc . Spec . Metrics . ServiceMonitor = & tsapi . ServiceMonitor { Enable : true , Labels : tsapi . Labels { "foo" : "bar" } }
2024-12-03 12:35:25 +00:00
} )
expectReconciled ( t , ingR , "default" , "test" )
2025-01-17 05:37:53 +00:00
expectEqual ( t , fc , expectedMetricsService ( opts ) )
2025-01-09 07:15:19 +00:00
2024-12-03 12:35:25 +00:00
// 3. Create ServiceMonitor CRD and reconcile- ServiceMonitor should get created
mustCreate ( t , fc , crd )
expectReconciled ( t , ingR , "default" , "test" )
2025-01-09 07:15:19 +00:00
opts . serviceMonitorLabels = tsapi . Labels { "foo" : "bar" }
2025-01-17 05:37:53 +00:00
expectEqual ( t , fc , expectedMetricsService ( opts ) )
2025-01-09 07:15:19 +00:00
expectEqualUnstructured ( t , fc , expectedServiceMonitor ( t , opts ) )
// 4. Update ServiceMonitor CRD and reconcile- ServiceMonitor should get updated
mustUpdate ( t , fc , pc . Namespace , pc . Name , func ( proxyClass * tsapi . ProxyClass ) {
proxyClass . Spec . Metrics . ServiceMonitor . Labels = nil
} )
expectReconciled ( t , ingR , "default" , "test" )
opts . serviceMonitorLabels = nil
opts . resourceVersion = "2"
2025-01-17 05:37:53 +00:00
expectEqual ( t , fc , expectedMetricsService ( opts ) )
2024-12-03 12:35:25 +00:00
expectEqualUnstructured ( t , fc , expectedServiceMonitor ( t , opts ) )
2025-01-09 07:15:19 +00:00
// 5. Disable metrics - metrics resources should get deleted.
mustUpdate ( t , fc , pc . Namespace , pc . Name , func ( proxyClass * tsapi . ProxyClass ) {
proxyClass . Spec . Metrics = nil
} )
expectReconciled ( t , ingR , "default" , "test" )
expectMissing [ corev1 . Service ] ( t , fc , "operator-ns" , metricsResourceName ( shortName ) )
// ServiceMonitor gets garbage collected when the Service is deleted - we cannot test that here.
2024-12-03 12:35:25 +00:00
}
2025-03-21 08:53:41 +00:00
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" )
}
} )
}
}
2025-04-17 16:14:34 +01:00
func TestEmptyPath ( t * testing . T ) {
testCases := [ ] struct {
name string
paths [ ] networkingv1 . HTTPIngressPath
expectedEvents [ ] string
} {
{
name : "empty_path_with_prefix_type" ,
paths : [ ] networkingv1 . HTTPIngressPath {
{
PathType : ptrPathType ( networkingv1 . PathTypePrefix ) ,
Path : "" ,
Backend : * backend ( ) ,
} ,
} ,
expectedEvents : [ ] string {
"Normal PathUndefined configured backend is missing a path, defaulting to '/'" ,
} ,
} ,
{
name : "empty_path_with_implementation_specific_type" ,
paths : [ ] networkingv1 . HTTPIngressPath {
{
PathType : ptrPathType ( networkingv1 . PathTypeImplementationSpecific ) ,
Path : "" ,
Backend : * backend ( ) ,
} ,
} ,
expectedEvents : [ ] string {
"Normal PathUndefined configured backend is missing a path, defaulting to '/'" ,
} ,
} ,
{
name : "empty_path_with_exact_type" ,
paths : [ ] networkingv1 . HTTPIngressPath {
{
PathType : ptrPathType ( networkingv1 . PathTypeExact ) ,
Path : "" ,
Backend : * backend ( ) ,
} ,
} ,
expectedEvents : [ ] string {
"Warning UnsupportedPathTypeExact Exact path type strict matching is currently not supported and requests will be routed as for Prefix path type. This behaviour might change in the future." ,
"Normal PathUndefined configured backend is missing a path, defaulting to '/'" ,
} ,
} ,
{
name : "two_competing_but_not_identical_paths_including_one_empty" ,
paths : [ ] networkingv1 . HTTPIngressPath {
{
PathType : ptrPathType ( networkingv1 . PathTypeImplementationSpecific ) ,
Path : "" ,
Backend : * backend ( ) ,
} ,
{
PathType : ptrPathType ( networkingv1 . PathTypeImplementationSpecific ) ,
Path : "/" ,
Backend : * backend ( ) ,
} ,
} ,
expectedEvents : [ ] string {
"Normal PathUndefined configured backend is missing a path, defaulting to '/'" ,
} ,
} ,
}
for _ , tt := range testCases {
t . Run ( tt . name , func ( t * testing . T ) {
fc := fake . NewFakeClient ( ingressClass ( ) )
ft := & fakeTSClient { }
fr := record . NewFakeRecorder ( 3 ) // bump this if you expect a test case to throw more events
fakeTsnetServer := & fakeTSNetServer { certDomains : [ ] string { "foo.com" } }
zl , err := zap . NewDevelopment ( )
if err != nil {
t . Fatal ( err )
}
ingR := & IngressReconciler {
recorder : fr ,
Client : fc ,
ssr : & tailscaleSTSReconciler {
Client : fc ,
tsClient : ft ,
tsnetServer : fakeTsnetServer ,
defaultTags : [ ] string { "tag:k8s" } ,
operatorNamespace : "operator-ns" ,
proxyImage : "tailscale/tailscale" ,
} ,
logger : zl . Sugar ( ) ,
}
// 1. Resources get created for regular Ingress
mustCreate ( t , fc , ingressWithPaths ( tt . paths ) )
mustCreate ( t , fc , service ( ) )
expectReconciled ( t , ingR , "default" , "test" )
fullName , shortName := findGenName ( t , fc , "default" , "test" , "ingress" )
mustCreate ( t , fc , & corev1 . Pod {
ObjectMeta : metav1 . ObjectMeta {
Name : fullName ,
Namespace : "operator-ns" ,
UID : "test-uid" ,
} ,
} )
opts := configOpts {
stsName : shortName ,
secretName : fullName ,
namespace : "default" ,
parentType : "ingress" ,
hostname : "foo" ,
app : kubetypes . AppIngressResource ,
}
serveConfig := & ipn . ServeConfig {
TCP : map [ uint16 ] * ipn . TCPPortHandler { 443 : { HTTPS : true } } ,
Web : map [ ipn . HostPort ] * ipn . WebServerConfig { "${TS_CERT_DOMAIN}:443" : { Handlers : map [ string ] * ipn . HTTPHandler { "/" : { Proxy : "http://1.2.3.4:8080/" } } } } ,
}
opts . serveConfig = serveConfig
expectEqual ( t , fc , expectedSecret ( t , fc , opts ) )
expectEqual ( t , fc , expectedHeadlessService ( shortName , "ingress" ) )
expectEqual ( t , fc , expectedSTSUserspace ( t , fc , opts ) , removeHashAnnotation , removeResourceReqs )
expectEvents ( t , fr , tt . expectedEvents )
} )
}
}
// ptrPathType is a helper function to return a pointer to the pathtype string (required for TestEmptyPath)
func ptrPathType ( p networkingv1 . PathType ) * networkingv1 . PathType {
return & p
}
2025-03-21 08:53:41 +00:00
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" ) ,
2025-04-17 16:14:34 +01:00
DefaultBackend : backend ( ) ,
TLS : [ ] networkingv1 . IngressTLS {
{ Hosts : [ ] string { "default-test" } } ,
} ,
} ,
}
}
func ingressWithPaths ( paths [ ] networkingv1 . HTTPIngressPath ) * 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" ) ,
Rules : [ ] networkingv1 . IngressRule {
{
Host : "foo.tailnetxyz.ts.net" ,
IngressRuleValue : networkingv1 . IngressRuleValue {
HTTP : & networkingv1 . HTTPIngressRuleValue {
Paths : paths ,
} ,
2025-03-21 08:53:41 +00:00
} ,
} ,
} ,
TLS : [ ] networkingv1 . IngressTLS {
2025-04-17 16:14:34 +01:00
{ Hosts : [ ] string { "foo.tailnetxyz.ts.net" } } ,
} ,
} ,
}
}
func backend ( ) * networkingv1 . IngressBackend {
return & networkingv1 . IngressBackend {
Service : & networkingv1 . IngressServiceBackend {
Name : "test" ,
Port : networkingv1 . ServiceBackendPort {
Number : 8080 ,
2025-03-21 08:53:41 +00:00
} ,
} ,
}
}