This commit contains the Kubernetes implementation of peer relays via the new `PeerRelay` CRD. It's a mega branch consisting of the commits of other PRs gone into this work: 1. https://github.com/tailscale/tailscale/pull/20211 2. https://github.com/tailscale/tailscale/pull/20329 3. https://github.com/tailscale/tailscale/pull/20423 4. https://github.com/tailscale/tailscale/pull/20503 An instance of the `PeerRelay` CRD deploys a `StatefulSet` of containerboot instances configured to advertise themselves as peer relays using the IP addresses configured via `LoadBalancer` services on each cloud provider (with some AWS specifics as it's less automatic than its competing cloud providers). Per replica, a `LoadBalancer` type `Service` resource is provisioned and its IP address is used to configure the respective relay. This has been tested with success in AWS, GCP & Azure and provides additional modification to `Service` resources via the CRD for any other kinds of deployment environments. It also contains some work that may appear to be duplication of what already exists within `cmd/k8s-operator` so we can start building an appropriate migration path for `Connector`, `ProxyGroup` etc into respective `k8s-operator/reconciler/*` packages. Closes https://github.com/tailscale/corp/issues/34524
142 lines
3.6 KiB
Go
142 lines
3.6 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build !plan9
|
|
|
|
package reconciler_test
|
|
|
|
import (
|
|
"maps"
|
|
"slices"
|
|
"testing"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
ctrlreconcile "sigs.k8s.io/controller-runtime/pkg/reconcile"
|
|
|
|
"tailscale.com/k8s-operator/reconciler"
|
|
)
|
|
|
|
func TestFinalizers(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
object := &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test",
|
|
Namespace: "test",
|
|
},
|
|
StringData: map[string]string{
|
|
"hello": "world",
|
|
},
|
|
}
|
|
|
|
reconciler.SetFinalizer(object)
|
|
|
|
if !slices.Contains(object.Finalizers, reconciler.FinalizerName) {
|
|
t.Fatalf("object does not have finalizer %q: %v", reconciler.FinalizerName, object.Finalizers)
|
|
}
|
|
|
|
reconciler.RemoveFinalizer(object)
|
|
|
|
if slices.Contains(object.Finalizers, reconciler.FinalizerName) {
|
|
t.Fatalf("object still has finalizer %q: %v", reconciler.FinalizerName, object.Finalizers)
|
|
}
|
|
}
|
|
|
|
func TestLabels(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("cluster-scoped-parent", func(t *testing.T) {
|
|
got := reconciler.Labels("peerrelay", "test", "")
|
|
want := map[string]string{
|
|
"tailscale.com/managed": "true",
|
|
"tailscale.com/parent-resource-type": "peerrelay",
|
|
"tailscale.com/parent-resource": "test",
|
|
}
|
|
if !maps.Equal(got, want) {
|
|
t.Errorf("expected %v, got %v", want, got)
|
|
}
|
|
})
|
|
|
|
t.Run("namespaced-parent", func(t *testing.T) {
|
|
got := reconciler.Labels("connector", "test", "kube-system")
|
|
want := map[string]string{
|
|
"tailscale.com/managed": "true",
|
|
"tailscale.com/parent-resource-type": "connector",
|
|
"tailscale.com/parent-resource": "test",
|
|
"tailscale.com/parent-resource-ns": "kube-system",
|
|
}
|
|
if !maps.Equal(got, want) {
|
|
t.Errorf("expected %v, got %v", want, got)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestEnqueueForChild(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
enqueue := reconciler.EnqueueForChild("peerrelay")
|
|
|
|
tests := []struct {
|
|
name string
|
|
labels map[string]string
|
|
want []ctrlreconcile.Request
|
|
}{
|
|
{
|
|
name: "matching-cluster-scoped",
|
|
labels: map[string]string{
|
|
"tailscale.com/managed": "true",
|
|
"tailscale.com/parent-resource-type": "peerrelay",
|
|
"tailscale.com/parent-resource": "test",
|
|
},
|
|
want: []ctrlreconcile.Request{{NamespacedName: types.NamespacedName{Name: "test"}}},
|
|
},
|
|
{
|
|
name: "matching-namespaced",
|
|
labels: map[string]string{
|
|
"tailscale.com/managed": "true",
|
|
"tailscale.com/parent-resource-type": "peerrelay",
|
|
"tailscale.com/parent-resource": "test",
|
|
"tailscale.com/parent-resource-ns": "kube-system",
|
|
},
|
|
want: []ctrlreconcile.Request{{NamespacedName: types.NamespacedName{Name: "test", Namespace: "kube-system"}}},
|
|
},
|
|
{
|
|
name: "not-managed",
|
|
labels: map[string]string{
|
|
"tailscale.com/parent-resource-type": "peerrelay",
|
|
"tailscale.com/parent-resource": "test",
|
|
},
|
|
},
|
|
{
|
|
name: "wrong-parent-type",
|
|
labels: map[string]string{
|
|
"tailscale.com/managed": "true",
|
|
"tailscale.com/parent-resource-type": "proxygroup",
|
|
"tailscale.com/parent-resource": "test",
|
|
},
|
|
},
|
|
{
|
|
name: "missing-parent-name",
|
|
labels: map[string]string{
|
|
"tailscale.com/managed": "true",
|
|
"tailscale.com/parent-resource-type": "peerrelay",
|
|
},
|
|
},
|
|
{
|
|
name: "no-labels",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
obj := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Labels: tc.labels}}
|
|
got := enqueue(t.Context(), obj)
|
|
if !slices.Equal(got, tc.want) {
|
|
t.Errorf("expected %v, got %v", tc.want, got)
|
|
}
|
|
})
|
|
}
|
|
}
|