You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
223 lines
7.1 KiB
223 lines
7.1 KiB
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build !plan9
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"slices"
|
|
"testing"
|
|
|
|
"go.uber.org/zap"
|
|
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"
|
|
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
|
|
|
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
|
|
"tailscale.com/k8s-operator/tsclient"
|
|
"tailscale.com/kube/kubetypes"
|
|
"tailscale.com/tstest"
|
|
)
|
|
|
|
func TestService_DefaultProxyClassInitiallyNotReady(t *testing.T) {
|
|
pc := &tsapi.ProxyClass{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "custom-metadata"},
|
|
Spec: tsapi.ProxyClassSpec{
|
|
TailscaleConfig: &tsapi.TailscaleConfig{
|
|
AcceptRoutes: true,
|
|
},
|
|
StatefulSet: &tsapi.StatefulSet{
|
|
Labels: tsapi.Labels{"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 := zap.Must(zap.NewDevelopment())
|
|
clock := tstest.NewClock(tstest.ClockOpts{})
|
|
sr := &ServiceReconciler{
|
|
Client: fc,
|
|
ssr: &tailscaleSTSReconciler{
|
|
Client: fc,
|
|
clients: tsclient.NewProvider(ft),
|
|
defaultTags: []string{"tag:k8s"},
|
|
operatorNamespace: "operator-ns",
|
|
proxyImage: "tailscale/tailscale",
|
|
},
|
|
defaultProxyClass: "custom-metadata",
|
|
logger: zl.Sugar(),
|
|
clock: clock,
|
|
}
|
|
|
|
// 1. A new tailscale LoadBalancer Service is created but the default
|
|
// ProxyClass is not ready yet.
|
|
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: new("tailscale"),
|
|
},
|
|
})
|
|
expectReconciled(t, sr, "default", "test")
|
|
labels := map[string]string{
|
|
kubetypes.LabelManaged: "true",
|
|
LabelParentName: "test",
|
|
LabelParentNamespace: "operator-ns",
|
|
LabelParentType: "svc",
|
|
}
|
|
s, err := getSingleObject[corev1.Secret](context.Background(), fc, "operator-ns", labels)
|
|
if err != nil {
|
|
t.Fatalf("finding Secret for %q: %v", "test", err)
|
|
}
|
|
if s != nil {
|
|
t.Fatalf("expected no Secret to be created when default ProxyClass is not ready, but found one: %v", s)
|
|
}
|
|
|
|
// 2. ProxyClass is set to Ready, the Service can become ready now.
|
|
mustUpdateStatus(t, fc, "", "custom-metadata", func(pc *tsapi.ProxyClass) {
|
|
pc.Status = tsapi.ProxyClassStatus{
|
|
Conditions: []metav1.Condition{{
|
|
Status: metav1.ConditionTrue,
|
|
Type: string(tsapi.ProxyClassReady),
|
|
ObservedGeneration: pc.Generation,
|
|
}},
|
|
}
|
|
})
|
|
expectReconciled(t, sr, "default", "test")
|
|
fullName, shortName := findGenName(t, fc, "default", "test", "svc")
|
|
opts := configOpts{
|
|
replicas: new(int32(1)),
|
|
stsName: shortName,
|
|
secretName: fullName,
|
|
namespace: "default",
|
|
parentType: "svc",
|
|
hostname: "default-test",
|
|
clusterTargetIP: "10.20.30.40",
|
|
app: kubetypes.AppIngressProxy,
|
|
proxyClass: pc.Name,
|
|
}
|
|
expectEqual(t, fc, expectedSecret(t, fc, opts))
|
|
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
|
|
expectEqual(t, fc, expectedSTS(t, fc, opts), removeResourceReqs)
|
|
}
|
|
|
|
func TestProxyClassHandlerForSvc(t *testing.T) {
|
|
svc := func(name string, annotations, labels map[string]string) *corev1.Service {
|
|
return &corev1.Service{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
Namespace: "default",
|
|
Annotations: annotations,
|
|
Labels: labels,
|
|
},
|
|
Spec: corev1.ServiceSpec{
|
|
ClusterIP: "1.2.3.4",
|
|
},
|
|
}
|
|
}
|
|
lbSvc := func(name string, annotations map[string]string, class *string) *corev1.Service {
|
|
return &corev1.Service{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
Namespace: "foo",
|
|
Annotations: annotations,
|
|
},
|
|
Spec: corev1.ServiceSpec{
|
|
Type: corev1.ServiceTypeLoadBalancer,
|
|
LoadBalancerClass: class,
|
|
ClusterIP: "1.2.3.4",
|
|
},
|
|
}
|
|
}
|
|
|
|
const (
|
|
defaultPCName = "default-proxyclass"
|
|
otherPCName = "other-proxyclass"
|
|
unreferencedPCName = "unreferenced-proxyclass"
|
|
)
|
|
fc := fake.NewClientBuilder().
|
|
WithScheme(tsapi.GlobalScheme).
|
|
WithIndex(&corev1.Service{}, indexServiceProxyClass, indexProxyClass).
|
|
WithIndex(&corev1.Service{}, indexServiceExposed, indexExposed).
|
|
WithIndex(&corev1.Service{}, indexServiceType, indexType).
|
|
WithObjects(
|
|
svc("not-exposed", nil, nil),
|
|
svc("exposed-default", map[string]string{AnnotationExpose: "true"}, nil),
|
|
svc("exposed-other", map[string]string{AnnotationExpose: "true", LabelAnnotationProxyClass: otherPCName}, nil),
|
|
svc("annotated", map[string]string{LabelAnnotationProxyClass: defaultPCName}, nil),
|
|
svc("labelled", nil, map[string]string{LabelAnnotationProxyClass: defaultPCName}),
|
|
lbSvc("lb-svc", nil, new("tailscale")),
|
|
lbSvc("lb-svc-no-class", nil, nil),
|
|
lbSvc("lb-svc-other-class", nil, new("other")),
|
|
lbSvc("lb-svc-other-pc", map[string]string{LabelAnnotationProxyClass: otherPCName}, nil),
|
|
).
|
|
Build()
|
|
|
|
zl := zap.Must(zap.NewDevelopment())
|
|
mapFunc := proxyClassHandlerForSvc(fc, zl.Sugar(), defaultPCName, true)
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
proxyClassName string
|
|
expected []reconcile.Request
|
|
}{
|
|
{
|
|
name: "default_ProxyClass",
|
|
proxyClassName: defaultPCName,
|
|
expected: []reconcile.Request{
|
|
{NamespacedName: types.NamespacedName{Namespace: "default", Name: "exposed-default"}},
|
|
{NamespacedName: types.NamespacedName{Namespace: "default", Name: "annotated"}},
|
|
{NamespacedName: types.NamespacedName{Namespace: "default", Name: "labelled"}},
|
|
{NamespacedName: types.NamespacedName{Namespace: "foo", Name: "lb-svc"}},
|
|
{NamespacedName: types.NamespacedName{Namespace: "foo", Name: "lb-svc-no-class"}},
|
|
},
|
|
},
|
|
{
|
|
name: "other_ProxyClass",
|
|
proxyClassName: otherPCName,
|
|
expected: []reconcile.Request{
|
|
{NamespacedName: types.NamespacedName{Namespace: "default", Name: "exposed-other"}},
|
|
{NamespacedName: types.NamespacedName{Namespace: "foo", Name: "lb-svc-other-pc"}},
|
|
},
|
|
},
|
|
{
|
|
name: "unreferenced_ProxyClass",
|
|
proxyClassName: unreferencedPCName,
|
|
expected: nil,
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
reqs := mapFunc(t.Context(), &tsapi.ProxyClass{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: tc.proxyClassName,
|
|
},
|
|
})
|
|
if len(reqs) != len(tc.expected) {
|
|
t.Fatalf("expected %d requests, got %d: %v", len(tc.expected), len(reqs), reqs)
|
|
}
|
|
for _, expected := range tc.expected {
|
|
if !slices.Contains(reqs, expected) {
|
|
t.Errorf("expected request for Service %q not found in results: %v", expected.Name, reqs)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|