diff --git a/cmd/k8s-operator/operator.go b/cmd/k8s-operator/operator.go index 3375e2664..139101cc8 100644 --- a/cmd/k8s-operator/operator.go +++ b/cmd/k8s-operator/operator.go @@ -183,6 +183,9 @@ waitOnline: // startReconcilers starts the controller-runtime manager and registers the // ServiceReconciler. func startReconcilers(zlog *zap.SugaredLogger, tsNamespace string, restConfig *rest.Config, tsClient *tailscale.Client, image, priorityClassName, tags string) { + var ( + isDefaultLoadBalancer = defaultBool("OPERATOR_DEFAULT_LOAD_BALANCER", false) + ) startlog := zlog.Named("startReconcilers") // For secrets and statefulsets, we only get permission to touch the objects // in the controller's own namespace. This cannot be expressed by @@ -234,9 +237,10 @@ func startReconcilers(zlog *zap.SugaredLogger, tsNamespace string, restConfig *r Watches(&appsv1.StatefulSet{}, reconcileFilter). Watches(&corev1.Secret{}, reconcileFilter). Complete(&ServiceReconciler{ - ssr: ssr, - Client: mgr.GetClient(), - logger: zlog.Named("service-reconciler"), + ssr: ssr, + Client: mgr.GetClient(), + logger: zlog.Named("service-reconciler"), + isDefaultLoadBalancer: isDefaultLoadBalancer, }) if err != nil { startlog.Fatalf("could not create controller: %v", err) diff --git a/cmd/k8s-operator/operator_test.go b/cmd/k8s-operator/operator_test.go index 3c0ace1f2..bcd0bbd50 100644 --- a/cmd/k8s-operator/operator_test.go +++ b/cmd/k8s-operator/operator_test.go @@ -650,6 +650,52 @@ func TestCustomPriorityClassName(t *testing.T) { expectEqual(t, fc, expectedSTS(shortName, fullName, "custom-priority-class-name", "tailscale-critical")) } +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") + + expectEqual(t, fc, expectedSecret(fullName)) + expectEqual(t, fc, expectedHeadlessService(shortName)) + expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test", "")) +} + func expectedSecret(name string) *corev1.Secret { return &corev1.Secret{ TypeMeta: metav1.TypeMeta{ @@ -780,6 +826,9 @@ func findGenName(t *testing.T, client client.Client, ns, name string) (full, noS if err != nil { t.Fatalf("finding secret for %q: %v", name, err) } + if s == nil { + t.Fatalf("no secret found for %q", name) + } return s.GetName(), strings.TrimSuffix(s.GetName(), "-0") } diff --git a/cmd/k8s-operator/svc.go b/cmd/k8s-operator/svc.go index ff6dcb770..8b777965d 100644 --- a/cmd/k8s-operator/svc.go +++ b/cmd/k8s-operator/svc.go @@ -20,8 +20,9 @@ import ( type ServiceReconciler struct { client.Client - ssr *tailscaleSTSReconciler - logger *zap.SugaredLogger + ssr *tailscaleSTSReconciler + logger *zap.SugaredLogger + isDefaultLoadBalancer bool } func childResourceLabels(name, ns, typ string) map[string]string { @@ -177,8 +178,8 @@ func (a *ServiceReconciler) shouldExpose(svc *corev1.Service) bool { func (a *ServiceReconciler) hasLoadBalancerClass(svc *corev1.Service) bool { return svc != nil && svc.Spec.Type == corev1.ServiceTypeLoadBalancer && - svc.Spec.LoadBalancerClass != nil && - *svc.Spec.LoadBalancerClass == "tailscale" + (svc.Spec.LoadBalancerClass != nil && *svc.Spec.LoadBalancerClass == "tailscale" || + svc.Spec.LoadBalancerClass == nil && a.isDefaultLoadBalancer) } func (a *ServiceReconciler) hasAnnotation(svc *corev1.Service) bool {